diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /js/src/builtin | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/builtin')
131 files changed, 84382 insertions, 0 deletions
diff --git a/js/src/builtin/.eslintrc.js b/js/src/builtin/.eslintrc.js new file mode 100644 index 0000000000..24063417e8 --- /dev/null +++ b/js/src/builtin/.eslintrc.js @@ -0,0 +1,157 @@ +/* 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/. */ + +"use strict"; + +module.exports = { + plugins: ["spidermonkey-js"], + + overrides: [ + { + files: ["*.js"], + excludedFiles: ".eslintrc.js", + processor: "spidermonkey-js/processor", + env: { + // Disable all built-in environments. + node: false, + browser: false, + builtin: false, + + // We need to explicitly disable the default environments added from + // "tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js". + es2021: false, + "mozilla/privileged": false, + "mozilla/specific": false, + + // Enable SpiderMonkey's self-hosted environment. + "spidermonkey-js/environment": true, + }, + + parserOptions: { + ecmaVersion: "latest", + sourceType: "script", + + // Self-hosted code defaults to strict mode. + ecmaFeatures: { + impliedStrict: true, + }, + + // Strict mode has to be enabled separately for the Babel parser. + babelOptions: { + parserOpts: { + strictMode: true, + }, + }, + }, + + rules: { + // We should fix those at some point, but we use this to detect NaNs. + "no-self-compare": "off", + "no-lonely-if": "off", + // Disabled until we can use let/const to fix those erorrs, and undefined + // names cause an exception and abort during runtime initialization. + "no-redeclare": "off", + // Disallow use of |void 0|. Instead use |undefined|. + "no-void": ["error", { allowAsStatement: true }], + // Disallow loose equality because of objects with the [[IsHTMLDDA]] + // internal slot, aka |document.all|, aka "objects emulating undefined". + eqeqeq: "error", + // All self-hosted code is implicitly strict mode, so there's no need to + // add a strict-mode directive. + strict: ["error", "never"], + // Disallow syntax not supported in self-hosted code. + "no-restricted-syntax": [ + "error", + { + selector: "ClassDeclaration", + message: "Class declarations are not allowed", + }, + { + selector: "ClassExpression", + message: "Class expressions are not allowed", + }, + { + selector: "Literal[regex]", + message: "Regular expression literals are not allowed", + }, + { + selector: "CallExpression > MemberExpression.callee", + message: + "Direct method calls are not allowed, use callFunction() or callContentFunction()", + }, + { + selector: "NewExpression > MemberExpression.callee", + message: + "Direct method calls are not allowed, use constructContentFunction()", + }, + { + selector: "YieldExpression[delegate=true]", + message: + "yield* is not allowed because it can run user-modifiable iteration code", + }, + { + selector: "ForOfStatement > :not(CallExpression).right", + message: + "for-of loops must use allowContentIter() or allowContentIterWith()", + }, + { + selector: + "ForOfStatement > CallExpression.right > :not(Identifier[name='allowContentIter'], Identifier[name='allowContentIterWith']).callee", + message: + "for-of loops must use allowContentIter() or allowContentIterWith()", + }, + { + selector: + "CallExpression[callee.name='TO_PROPERTY_KEY'] > :not(Identifier).arguments:first-child", + message: + "TO_PROPERTY_KEY macro must be called with a simple identifier", + }, + { + selector: "Identifier[name='arguments']", + message: + "'arguments' is disallowed, use ArgumentsLength(), GetArgument(n), or rest-parameters", + }, + ], + }, + + globals: { + // The bytecode compiler special-cases these identifiers. + ArgumentsLength: "readonly", + allowContentIter: "readonly", + allowContentIterWith: "readonly", + callContentFunction: "readonly", + callFunction: "readonly", + constructContentFunction: "readonly", + DefineDataProperty: "readonly", + forceInterpreter: "readonly", + GetArgument: "readonly", + GetBuiltinConstructor: "readonly", + GetBuiltinPrototype: "readonly", + GetBuiltinSymbol: "readonly", + getPropertySuper: "readonly", + hasOwn: "readonly", + resumeGenerator: "readonly", + SetCanonicalName: "readonly", + SetIsInlinableLargeFunction: "readonly", + ToNumeric: "readonly", + ToString: "readonly", + IsNullOrUndefined: "readonly", + + // We've disabled all built-in environments, which also removed + // `undefined` from the list of globals. Put it back because it's + // actually allowed in self-hosted code. + undefined: "readonly", + + // Disable globals from stage 2/3 proposals for which we have work in + // progress patches. Eventually these will be part of a future ES + // release, in which case we can remove these extra entries. + AsyncIterator: "off", + Iterator: "off", + Record: "off", + Temporal: "off", + Tuple: "off", + }, + }, + ], +}; diff --git a/js/src/builtin/Array-inl.h b/js/src/builtin/Array-inl.h new file mode 100644 index 0000000000..b3210402ab --- /dev/null +++ b/js/src/builtin/Array-inl.h @@ -0,0 +1,40 @@ +/* -*- 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 builtin_Array_inl_h +#define builtin_Array_inl_h + +#include "builtin/Array.h" + +#include "vm/JSObject.h" + +#include "vm/ArgumentsObject-inl.h" +#include "vm/ObjectOperations-inl.h" + +namespace js { + +inline bool GetElement(JSContext* cx, HandleObject obj, uint32_t index, + MutableHandleValue vp) { + if (obj->is<NativeObject>() && + index < obj->as<NativeObject>().getDenseInitializedLength()) { + vp.set(obj->as<NativeObject>().getDenseElement(index)); + if (!vp.isMagic(JS_ELEMENTS_HOLE)) { + return true; + } + } + + if (obj->is<ArgumentsObject>()) { + if (obj->as<ArgumentsObject>().maybeGetElement(index, vp)) { + return true; + } + } + + return GetElement(cx, obj, obj, index, vp); +} + +} // namespace js + +#endif // builtin_Array_inl_h diff --git a/js/src/builtin/Array.cpp b/js/src/builtin/Array.cpp new file mode 100644 index 0000000000..24d13c118e --- /dev/null +++ b/js/src/builtin/Array.cpp @@ -0,0 +1,5562 @@ +/* -*- 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 "builtin/Array-inl.h" + +#include "mozilla/CheckedInt.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Maybe.h" +#include "mozilla/SIMD.h" +#include "mozilla/TextUtils.h" + +#include <algorithm> +#include <cmath> +#include <iterator> + +#include "jsfriendapi.h" +#include "jsnum.h" +#include "jstypes.h" + +#include "ds/Sort.h" +#include "gc/Allocator.h" +#include "jit/InlinableNatives.h" +#include "js/Class.h" +#include "js/Conversions.h" +#include "js/experimental/JitInfo.h" // JSJitGetterOp, JSJitInfo +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/PropertySpec.h" +#include "util/Poison.h" +#include "util/StringBuffer.h" +#include "util/Text.h" +#include "vm/ArgumentsObject.h" +#include "vm/EqualityOperations.h" +#include "vm/Interpreter.h" +#include "vm/Iteration.h" +#include "vm/JSContext.h" +#include "vm/JSFunction.h" +#include "vm/JSObject.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/SelfHosting.h" +#include "vm/Shape.h" +#include "vm/ToSource.h" // js::ValueToSource +#include "vm/TypedArrayObject.h" +#include "vm/WellKnownAtom.h" // js_*_str +#include "vm/WrapperObject.h" +#ifdef ENABLE_RECORD_TUPLE +# include "vm/TupleType.h" +#endif + +#include "vm/ArgumentsObject-inl.h" +#include "vm/ArrayObject-inl.h" +#include "vm/GeckoProfiler-inl.h" +#include "vm/IsGivenTypeObject-inl.h" +#include "vm/JSAtom-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::Abs; +using mozilla::CeilingLog2; +using mozilla::CheckedInt; +using mozilla::DebugOnly; +using mozilla::IsAsciiDigit; +using mozilla::Maybe; +using mozilla::SIMD; + +using JS::AutoCheckCannotGC; +using JS::IsArrayAnswer; +using JS::ToUint32; + +static inline bool ObjectMayHaveExtraIndexedOwnProperties(JSObject* obj) { + if (!obj->is<NativeObject>()) { + return true; + } + + if (obj->as<NativeObject>().isIndexed()) { + return true; + } + + if (obj->is<TypedArrayObject>()) { + return true; + } + + return ClassMayResolveId(*obj->runtimeFromAnyThread()->commonNames, + obj->getClass(), PropertyKey::Int(0), obj); +} + +bool js::PrototypeMayHaveIndexedProperties(NativeObject* obj) { + do { + MOZ_ASSERT(obj->hasStaticPrototype(), + "dynamic-prototype objects must be non-native"); + + JSObject* proto = obj->staticPrototype(); + if (!proto) { + return false; // no extra indexed properties found + } + + if (ObjectMayHaveExtraIndexedOwnProperties(proto)) { + return true; + } + obj = &proto->as<NativeObject>(); + if (obj->getDenseInitializedLength() != 0) { + return true; + } + } while (true); +} + +/* + * Whether obj may have indexed properties anywhere besides its dense + * elements. This includes other indexed properties in its shape hierarchy, and + * indexed properties or elements along its prototype chain. + */ +static bool ObjectMayHaveExtraIndexedProperties(JSObject* obj) { + MOZ_ASSERT_IF(obj->hasDynamicPrototype(), !obj->is<NativeObject>()); + + if (ObjectMayHaveExtraIndexedOwnProperties(obj)) { + return true; + } + + return PrototypeMayHaveIndexedProperties(&obj->as<NativeObject>()); +} + +bool JS::IsArray(JSContext* cx, HandleObject obj, IsArrayAnswer* answer) { + if (obj->is<ArrayObject>()) { + *answer = IsArrayAnswer::Array; + return true; + } + + if (obj->is<ProxyObject>()) { + return Proxy::isArray(cx, obj, answer); + } + + *answer = IsArrayAnswer::NotArray; + return true; +} + +bool JS::IsArray(JSContext* cx, HandleObject obj, bool* isArray) { + IsArrayAnswer answer; + if (!IsArray(cx, obj, &answer)) { + return false; + } + + if (answer == IsArrayAnswer::RevokedProxy) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_PROXY_REVOKED); + return false; + } + + *isArray = answer == IsArrayAnswer::Array; + return true; +} + +bool js::IsArrayFromJit(JSContext* cx, HandleObject obj, bool* isArray) { + return JS::IsArray(cx, obj, isArray); +} + +// ES2017 7.1.15 ToLength. +bool js::ToLength(JSContext* cx, HandleValue v, uint64_t* out) { + if (v.isInt32()) { + int32_t i = v.toInt32(); + *out = i < 0 ? 0 : i; + return true; + } + + double d; + if (v.isDouble()) { + d = v.toDouble(); + } else { + if (!ToNumber(cx, v, &d)) { + return false; + } + } + + d = JS::ToInteger(d); + if (d <= 0.0) { + *out = 0; + } else { + *out = uint64_t(std::min(d, DOUBLE_INTEGRAL_PRECISION_LIMIT - 1)); + } + return true; +} + +bool js::GetLengthProperty(JSContext* cx, HandleObject obj, uint64_t* lengthp) { + if (obj->is<ArrayObject>()) { + *lengthp = obj->as<ArrayObject>().length(); + return true; + } + + if (obj->is<ArgumentsObject>()) { + ArgumentsObject& argsobj = obj->as<ArgumentsObject>(); + if (!argsobj.hasOverriddenLength()) { + *lengthp = argsobj.initialLength(); + return true; + } + } + + RootedValue value(cx); + if (!GetProperty(cx, obj, obj, cx->names().length, &value)) { + return false; + } + + return ToLength(cx, value, lengthp); +} + +// Fast path for array functions where the object is expected to be an array. +static MOZ_ALWAYS_INLINE bool GetLengthPropertyInlined(JSContext* cx, + HandleObject obj, + uint64_t* lengthp) { + if (obj->is<ArrayObject>()) { + *lengthp = obj->as<ArrayObject>().length(); + return true; + } + + return GetLengthProperty(cx, obj, lengthp); +} + +/* + * Determine if the id represents an array index. + * + * An id is an array index according to ECMA by (15.4): + * + * "Array objects give special treatment to a certain class of property names. + * A property name P (in the form of a string value) is an array index if and + * only if ToString(ToUint32(P)) is equal to P and ToUint32(P) is not equal + * to 2^32-1." + * + * This means the largest allowed index is actually 2^32-2 (4294967294). + * + * In our implementation, it would be sufficient to check for id.isInt32() + * except that by using signed 31-bit integers we miss the top half of the + * valid range. This function checks the string representation itself; note + * that calling a standard conversion routine might allow strings such as + * "08" or "4.0" as array indices, which they are not. + * + */ +JS_PUBLIC_API bool js::StringIsArrayIndex(JSLinearString* str, + uint32_t* indexp) { + if (!str->isIndex(indexp)) { + return false; + } + MOZ_ASSERT(*indexp <= MAX_ARRAY_INDEX); + return true; +} + +JS_PUBLIC_API bool js::StringIsArrayIndex(const char16_t* str, uint32_t length, + uint32_t* indexp) { + if (length == 0 || length > UINT32_CHAR_BUFFER_LENGTH) { + return false; + } + if (!mozilla::IsAsciiDigit(str[0])) { + return false; + } + if (!CheckStringIsIndex(str, length, indexp)) { + return false; + } + MOZ_ASSERT(*indexp <= MAX_ARRAY_INDEX); + return true; +} + +template <typename T> +static bool ToId(JSContext* cx, T index, MutableHandleId id); + +template <> +bool ToId(JSContext* cx, uint32_t index, MutableHandleId id) { + return IndexToId(cx, index, id); +} + +template <> +bool ToId(JSContext* cx, uint64_t index, MutableHandleId id) { + MOZ_ASSERT(index < uint64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)); + + if (index == uint32_t(index)) { + return IndexToId(cx, uint32_t(index), id); + } + + Value tmp = DoubleValue(index); + return PrimitiveValueToId<CanGC>(cx, HandleValue::fromMarkedLocation(&tmp), + id); +} + +/* + * If the property at the given index exists, get its value into |vp| and set + * |*hole| to false. Otherwise set |*hole| to true and |vp| to Undefined. + */ +template <typename T> +static bool HasAndGetElement(JSContext* cx, HandleObject obj, + HandleObject receiver, T index, bool* hole, + MutableHandleValue vp) { + if (obj->is<NativeObject>()) { + NativeObject* nobj = &obj->as<NativeObject>(); + if (index < nobj->getDenseInitializedLength()) { + vp.set(nobj->getDenseElement(size_t(index))); + if (!vp.isMagic(JS_ELEMENTS_HOLE)) { + *hole = false; + return true; + } + } + if (nobj->is<ArgumentsObject>() && index <= UINT32_MAX) { + if (nobj->as<ArgumentsObject>().maybeGetElement(uint32_t(index), vp)) { + *hole = false; + return true; + } + } + } + + RootedId id(cx); + if (!ToId(cx, index, &id)) { + return false; + } + + bool found; + if (!HasProperty(cx, obj, id, &found)) { + return false; + } + + if (found) { + if (!GetProperty(cx, obj, receiver, id, vp)) { + return false; + } + } else { + vp.setUndefined(); + } + *hole = !found; + return true; +} + +template <typename T> +static inline bool HasAndGetElement(JSContext* cx, HandleObject obj, T index, + bool* hole, MutableHandleValue vp) { + return HasAndGetElement(cx, obj, obj, index, hole, vp); +} + +bool ElementAdder::append(JSContext* cx, HandleValue v) { + MOZ_ASSERT(index_ < length_); + if (resObj_) { + NativeObject* resObj = &resObj_->as<NativeObject>(); + DenseElementResult result = + resObj->setOrExtendDenseElements(cx, index_, v.address(), 1); + if (result == DenseElementResult::Failure) { + return false; + } + if (result == DenseElementResult::Incomplete) { + if (!DefineDataElement(cx, resObj_, index_, v)) { + return false; + } + } + } else { + vp_[index_] = v; + } + index_++; + return true; +} + +void ElementAdder::appendHole() { + MOZ_ASSERT(getBehavior_ == ElementAdder::CheckHasElemPreserveHoles); + MOZ_ASSERT(index_ < length_); + if (!resObj_) { + vp_[index_].setMagic(JS_ELEMENTS_HOLE); + } + index_++; +} + +bool js::GetElementsWithAdder(JSContext* cx, HandleObject obj, + HandleObject receiver, uint32_t begin, + uint32_t end, ElementAdder* adder) { + MOZ_ASSERT(begin <= end); + + RootedValue val(cx); + for (uint32_t i = begin; i < end; i++) { + if (adder->getBehavior() == ElementAdder::CheckHasElemPreserveHoles) { + bool hole; + if (!HasAndGetElement(cx, obj, receiver, i, &hole, &val)) { + return false; + } + if (hole) { + adder->appendHole(); + continue; + } + } else { + MOZ_ASSERT(adder->getBehavior() == ElementAdder::GetElement); + if (!GetElement(cx, obj, receiver, i, &val)) { + return false; + } + } + if (!adder->append(cx, val)) { + return false; + } + } + + return true; +} + +static inline bool IsPackedArrayOrNoExtraIndexedProperties(JSObject* obj, + uint64_t length) { + return (IsPackedArray(obj) && obj->as<ArrayObject>().length() == length) || + !ObjectMayHaveExtraIndexedProperties(obj); +} + +static bool GetDenseElements(NativeObject* aobj, uint32_t length, Value* vp) { + MOZ_ASSERT(IsPackedArrayOrNoExtraIndexedProperties(aobj, length)); + + if (length > aobj->getDenseInitializedLength()) { + return false; + } + + for (size_t i = 0; i < length; i++) { + vp[i] = aobj->getDenseElement(i); + + // No other indexed properties so hole => undefined. + if (vp[i].isMagic(JS_ELEMENTS_HOLE)) { + vp[i] = UndefinedValue(); + } + } + + return true; +} + +bool js::GetElements(JSContext* cx, HandleObject aobj, uint32_t length, + Value* vp) { + if (IsPackedArrayOrNoExtraIndexedProperties(aobj, length)) { + if (GetDenseElements(&aobj->as<NativeObject>(), length, vp)) { + return true; + } + } + + if (aobj->is<ArgumentsObject>()) { + ArgumentsObject& argsobj = aobj->as<ArgumentsObject>(); + if (!argsobj.hasOverriddenLength()) { + if (argsobj.maybeGetElements(0, length, vp)) { + return true; + } + } + } + + if (aobj->is<TypedArrayObject>()) { + Handle<TypedArrayObject*> typedArray = aobj.as<TypedArrayObject>(); + if (typedArray->length() == length) { + return TypedArrayObject::getElements(cx, typedArray, vp); + } + } + + if (js::GetElementsOp op = aobj->getOpsGetElements()) { + ElementAdder adder(cx, vp, length, ElementAdder::GetElement); + return op(cx, aobj, 0, length, &adder); + } + + for (uint32_t i = 0; i < length; i++) { + if (!GetElement(cx, aobj, aobj, i, + MutableHandleValue::fromMarkedLocation(&vp[i]))) { + return false; + } + } + + return true; +} + +static inline bool GetArrayElement(JSContext* cx, HandleObject obj, + uint64_t index, MutableHandleValue vp) { + if (obj->is<NativeObject>()) { + NativeObject* nobj = &obj->as<NativeObject>(); + if (index < nobj->getDenseInitializedLength()) { + vp.set(nobj->getDenseElement(size_t(index))); + if (!vp.isMagic(JS_ELEMENTS_HOLE)) { + return true; + } + } + + if (nobj->is<ArgumentsObject>() && index <= UINT32_MAX) { + if (nobj->as<ArgumentsObject>().maybeGetElement(uint32_t(index), vp)) { + return true; + } + } + } + + RootedId id(cx); + if (!ToId(cx, index, &id)) { + return false; + } + return GetProperty(cx, obj, obj, id, vp); +} + +static inline bool DefineArrayElement(JSContext* cx, HandleObject obj, + uint64_t index, HandleValue value) { + RootedId id(cx); + if (!ToId(cx, index, &id)) { + return false; + } + return DefineDataProperty(cx, obj, id, value); +} + +// Set the value of the property at the given index to v. +static inline bool SetArrayElement(JSContext* cx, HandleObject obj, + uint64_t index, HandleValue v) { + RootedId id(cx); + if (!ToId(cx, index, &id)) { + return false; + } + + return SetProperty(cx, obj, id, v); +} + +/* + * Attempt to delete the element |index| from |obj| as if by + * |obj.[[Delete]](index)|. + * + * If an error occurs while attempting to delete the element (that is, the call + * to [[Delete]] threw), return false. + * + * Otherwise call result.succeed() or result.fail() to indicate whether the + * deletion attempt succeeded (that is, whether the call to [[Delete]] returned + * true or false). (Deletes generally fail only when the property is + * non-configurable, but proxies may implement different semantics.) + */ +static bool DeleteArrayElement(JSContext* cx, HandleObject obj, uint64_t index, + ObjectOpResult& result) { + if (obj->is<ArrayObject>() && !obj->as<NativeObject>().isIndexed() && + !obj->as<NativeObject>().denseElementsAreSealed()) { + ArrayObject* aobj = &obj->as<ArrayObject>(); + if (index <= UINT32_MAX) { + uint32_t idx = uint32_t(index); + if (idx < aobj->getDenseInitializedLength()) { + if (idx + 1 == aobj->getDenseInitializedLength()) { + aobj->setDenseInitializedLengthMaybeNonExtensible(cx, idx); + } else { + aobj->setDenseElementHole(idx); + } + if (!SuppressDeletedElement(cx, obj, idx)) { + return false; + } + } + } + + return result.succeed(); + } + + RootedId id(cx); + if (!ToId(cx, index, &id)) { + return false; + } + return DeleteProperty(cx, obj, id, result); +} + +/* ES6 draft rev 32 (2 Febr 2015) 7.3.7 */ +static bool DeletePropertyOrThrow(JSContext* cx, HandleObject obj, + uint64_t index) { + ObjectOpResult success; + if (!DeleteArrayElement(cx, obj, index, success)) { + return false; + } + if (!success) { + RootedId id(cx); + if (!ToId(cx, index, &id)) { + return false; + } + return success.reportError(cx, obj, id); + } + return true; +} + +static bool DeletePropertiesOrThrow(JSContext* cx, HandleObject obj, + uint64_t len, uint64_t finalLength) { + if (obj->is<ArrayObject>() && !obj->as<NativeObject>().isIndexed() && + !obj->as<NativeObject>().denseElementsAreSealed()) { + if (len <= UINT32_MAX) { + // Skip forward to the initialized elements of this array. + len = std::min(uint32_t(len), + obj->as<ArrayObject>().getDenseInitializedLength()); + } + } + + for (uint64_t k = len; k > finalLength; k--) { + if (!CheckForInterrupt(cx)) { + return false; + } + + if (!DeletePropertyOrThrow(cx, obj, k - 1)) { + return false; + } + } + return true; +} + +static bool SetArrayLengthProperty(JSContext* cx, Handle<ArrayObject*> obj, + HandleValue value) { + RootedId id(cx, NameToId(cx->names().length)); + ObjectOpResult result; + if (obj->lengthIsWritable()) { + Rooted<PropertyDescriptor> desc( + cx, PropertyDescriptor::Data(value, JS::PropertyAttribute::Writable)); + if (!ArraySetLength(cx, obj, id, desc, result)) { + return false; + } + } else { + MOZ_ALWAYS_TRUE(result.fail(JSMSG_READ_ONLY)); + } + return result.checkStrict(cx, obj, id); +} + +static bool SetLengthProperty(JSContext* cx, HandleObject obj, + uint64_t length) { + MOZ_ASSERT(length < uint64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)); + + RootedValue v(cx, NumberValue(length)); + if (obj->is<ArrayObject>()) { + return SetArrayLengthProperty(cx, obj.as<ArrayObject>(), v); + } + return SetProperty(cx, obj, cx->names().length, v); +} + +bool js::SetLengthProperty(JSContext* cx, HandleObject obj, uint32_t length) { + RootedValue v(cx, NumberValue(length)); + if (obj->is<ArrayObject>()) { + return SetArrayLengthProperty(cx, obj.as<ArrayObject>(), v); + } + return SetProperty(cx, obj, cx->names().length, v); +} + +bool js::ArrayLengthGetter(JSContext* cx, HandleObject obj, HandleId id, + MutableHandleValue vp) { + MOZ_ASSERT(id == NameToId(cx->names().length)); + + vp.setNumber(obj->as<ArrayObject>().length()); + return true; +} + +bool js::ArrayLengthSetter(JSContext* cx, HandleObject obj, HandleId id, + HandleValue v, ObjectOpResult& result) { + MOZ_ASSERT(id == NameToId(cx->names().length)); + + Handle<ArrayObject*> arr = obj.as<ArrayObject>(); + MOZ_ASSERT(arr->lengthIsWritable(), + "setter shouldn't be called if property is non-writable"); + + Rooted<PropertyDescriptor> desc( + cx, PropertyDescriptor::Data(v, JS::PropertyAttribute::Writable)); + return ArraySetLength(cx, arr, id, desc, result); +} + +struct ReverseIndexComparator { + bool operator()(const uint32_t& a, const uint32_t& b, bool* lessOrEqualp) { + MOZ_ASSERT(a != b, "how'd we get duplicate indexes?"); + *lessOrEqualp = b <= a; + return true; + } +}; + +/* ES6 draft rev 34 (2015 Feb 20) 9.4.2.4 ArraySetLength */ +bool js::ArraySetLength(JSContext* cx, Handle<ArrayObject*> arr, HandleId id, + Handle<PropertyDescriptor> desc, + ObjectOpResult& result) { + MOZ_ASSERT(id == NameToId(cx->names().length)); + MOZ_ASSERT(desc.isDataDescriptor() || desc.isGenericDescriptor()); + + // Step 1. + uint32_t newLen; + if (!desc.hasValue()) { + // The spec has us calling OrdinaryDefineOwnProperty if + // Desc.[[Value]] is absent, but our implementation is so different that + // this is impossible. Instead, set newLen to the current length and + // proceed to step 9. + newLen = arr->length(); + } else { + // Step 2 is irrelevant in our implementation. + + // Step 3. + if (!ToUint32(cx, desc.value(), &newLen)) { + return false; + } + + // Step 4. + double d; + if (!ToNumber(cx, desc.value(), &d)) { + return false; + } + + // Step 5. + if (d != newLen) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_ARRAY_LENGTH); + return false; + } + + // Steps 6-8 are irrelevant in our implementation. + } + + // Steps 9-11. + bool lengthIsWritable = arr->lengthIsWritable(); +#ifdef DEBUG + { + mozilla::Maybe<PropertyInfo> lengthProp = arr->lookupPure(id); + MOZ_ASSERT(lengthProp.isSome()); + MOZ_ASSERT(lengthProp->writable() == lengthIsWritable); + } +#endif + uint32_t oldLen = arr->length(); + + // Part of steps 1.a, 12.a, and 16: Fail if we're being asked to change + // enumerability or configurability, or otherwise break the object + // invariants. (ES6 checks these by calling OrdinaryDefineOwnProperty, but + // in SM, the array length property is hardly ordinary.) + if ((desc.hasConfigurable() && desc.configurable()) || + (desc.hasEnumerable() && desc.enumerable()) || + (!lengthIsWritable && desc.hasWritable() && desc.writable())) { + return result.fail(JSMSG_CANT_REDEFINE_PROP); + } + + // Steps 12-13 for arrays with non-writable length. + if (!lengthIsWritable) { + if (newLen == oldLen) { + return result.succeed(); + } + + return result.fail(JSMSG_CANT_REDEFINE_ARRAY_LENGTH); + } + + // Step 19. + bool succeeded = true; + do { + // The initialized length and capacity of an array only need updating + // when non-hole elements are added or removed, which doesn't happen + // when array length stays the same or increases. + if (newLen >= oldLen) { + break; + } + + // Attempt to propagate dense-element optimization tricks, if possible, + // and avoid the generic (and accordingly slow) deletion code below. + // We can only do this if there are only densely-indexed elements. + // Once there's a sparse indexed element, there's no good way to know, + // save by enumerating all the properties to find it. But we *have* to + // know in case that sparse indexed element is non-configurable, as + // that element must prevent any deletions below it. Bug 586842 should + // fix this inefficiency by moving indexed storage to be entirely + // separate from non-indexed storage. + // A second reason for this optimization to be invalid is an active + // for..in iteration over the array. Keys deleted before being reached + // during the iteration must not be visited, and suppressing them here + // would be too costly. + // This optimization is also invalid when there are sealed + // (non-configurable) elements. + if (!arr->isIndexed() && !arr->denseElementsMaybeInIteration() && + !arr->denseElementsAreSealed()) { + uint32_t oldCapacity = arr->getDenseCapacity(); + uint32_t oldInitializedLength = arr->getDenseInitializedLength(); + MOZ_ASSERT(oldCapacity >= oldInitializedLength); + if (oldInitializedLength > newLen) { + arr->setDenseInitializedLengthMaybeNonExtensible(cx, newLen); + } + if (oldCapacity > newLen) { + if (arr->isExtensible()) { + arr->shrinkElements(cx, newLen); + } else { + MOZ_ASSERT(arr->getDenseInitializedLength() == + arr->getDenseCapacity()); + } + } + + // We've done the work of deleting any dense elements needing + // deletion, and there are no sparse elements. Thus we can skip + // straight to defining the length. + break; + } + + // Step 15. + // + // Attempt to delete all elements above the new length, from greatest + // to least. If any of these deletions fails, we're supposed to define + // the length to one greater than the index that couldn't be deleted, + // *with the property attributes specified*. This might convert the + // length to be not the value specified, yet non-writable. (You may be + // forgiven for thinking these are interesting semantics.) Example: + // + // var arr = + // Object.defineProperty([0, 1, 2, 3], 1, { writable: false }); + // Object.defineProperty(arr, "length", + // { value: 0, writable: false }); + // + // will convert |arr| to an array of non-writable length two, then + // throw a TypeError. + // + // We implement this behavior, in the relevant lops below, by setting + // |succeeded| to false. Then we exit the loop, define the length + // appropriately, and only then throw a TypeError, if necessary. + uint32_t gap = oldLen - newLen; + const uint32_t RemoveElementsFastLimit = 1 << 24; + if (gap < RemoveElementsFastLimit) { + // If we're removing a relatively small number of elements, just do + // it exactly by the spec. + while (newLen < oldLen) { + // Step 15a. + oldLen--; + + // Steps 15b-d. + ObjectOpResult deleteSucceeded; + if (!DeleteElement(cx, arr, oldLen, deleteSucceeded)) { + return false; + } + if (!deleteSucceeded) { + newLen = oldLen + 1; + succeeded = false; + break; + } + } + } else { + // If we're removing a large number of elements from an array + // that's probably sparse, try a different tack. Get all the own + // property names, sift out the indexes in the deletion range into + // a vector, sort the vector greatest to least, then delete the + // indexes greatest to least using that vector. See bug 322135. + // + // This heuristic's kind of a huge guess -- "large number of + // elements" and "probably sparse" are completely unprincipled + // predictions. In the long run, bug 586842 will support the right + // fix: store sparse elements in a sorted data structure that + // permits fast in-reverse-order traversal and concurrent removals. + + Vector<uint32_t> indexes(cx); + { + RootedIdVector props(cx); + if (!GetPropertyKeys(cx, arr, JSITER_OWNONLY | JSITER_HIDDEN, &props)) { + return false; + } + + for (size_t i = 0; i < props.length(); i++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + uint32_t index; + if (!IdIsIndex(props[i], &index)) { + continue; + } + + if (index >= newLen && index < oldLen) { + if (!indexes.append(index)) { + return false; + } + } + } + } + + uint32_t count = indexes.length(); + { + // We should use radix sort to be O(n), but this is uncommon + // enough that we'll punt til someone complains. + Vector<uint32_t> scratch(cx); + if (!scratch.resize(count)) { + return false; + } + MOZ_ALWAYS_TRUE(MergeSort(indexes.begin(), count, scratch.begin(), + ReverseIndexComparator())); + } + + uint32_t index = UINT32_MAX; + for (uint32_t i = 0; i < count; i++) { + MOZ_ASSERT(indexes[i] < index, "indexes should never repeat"); + index = indexes[i]; + + // Steps 15b-d. + ObjectOpResult deleteSucceeded; + if (!DeleteElement(cx, arr, index, deleteSucceeded)) { + return false; + } + if (!deleteSucceeded) { + newLen = index + 1; + succeeded = false; + break; + } + } + } + } while (false); + + // Update array length. Technically we should have been doing this + // throughout the loop, in step 19.d.iii. + arr->setLength(newLen); + + // Step 20. + if (desc.hasWritable() && !desc.writable()) { + Maybe<PropertyInfo> lengthProp = arr->lookup(cx, id); + MOZ_ASSERT(lengthProp.isSome()); + MOZ_ASSERT(lengthProp->isCustomDataProperty()); + PropertyFlags flags = lengthProp->flags(); + flags.clearFlag(PropertyFlag::Writable); + if (!NativeObject::changeCustomDataPropAttributes(cx, arr, id, flags)) { + return false; + } + } + + // All operations past here until the |!succeeded| code must be infallible, + // so that all element fields remain properly synchronized. + + // Trim the initialized length, if needed, to preserve the <= length + // invariant. (Capacity was already reduced during element deletion, if + // necessary.) + ObjectElements* header = arr->getElementsHeader(); + header->initializedLength = std::min(header->initializedLength, newLen); + + if (!arr->isExtensible()) { + arr->shrinkCapacityToInitializedLength(cx); + } + + if (desc.hasWritable() && !desc.writable()) { + arr->setNonWritableLength(cx); + } + + if (!succeeded) { + return result.fail(JSMSG_CANT_TRUNCATE_ARRAY); + } + + return result.succeed(); +} + +static bool array_addProperty(JSContext* cx, HandleObject obj, HandleId id, + HandleValue v) { + ArrayObject* arr = &obj->as<ArrayObject>(); + + uint32_t index; + if (!IdIsIndex(id, &index)) { + return true; + } + + uint32_t length = arr->length(); + if (index >= length) { + MOZ_ASSERT(arr->lengthIsWritable(), + "how'd this element get added if length is non-writable?"); + arr->setLength(index + 1); + } + return true; +} + +static SharedShape* AddLengthProperty(JSContext* cx, + Handle<SharedShape*> shape) { + // Add the 'length' property for a newly created array shape. + + MOZ_ASSERT(shape->propMapLength() == 0); + MOZ_ASSERT(shape->getObjectClass() == &ArrayObject::class_); + + RootedId lengthId(cx, NameToId(cx->names().length)); + constexpr PropertyFlags flags = {PropertyFlag::CustomDataProperty, + PropertyFlag::Writable}; + + Rooted<SharedPropMap*> map(cx, shape->propMap()); + uint32_t mapLength = shape->propMapLength(); + ObjectFlags objectFlags = shape->objectFlags(); + + if (!SharedPropMap::addCustomDataProperty(cx, &ArrayObject::class_, &map, + &mapLength, lengthId, flags, + &objectFlags)) { + return nullptr; + } + + return SharedShape::getPropMapShape(cx, shape->base(), shape->numFixedSlots(), + map, mapLength, objectFlags); +} + +static bool IsArrayConstructor(const JSObject* obj) { + // Note: this also returns true for cross-realm Array constructors in the + // same compartment. + return IsNativeFunction(obj, ArrayConstructor); +} + +static bool IsArrayConstructor(const Value& v) { + return v.isObject() && IsArrayConstructor(&v.toObject()); +} + +bool js::IsCrossRealmArrayConstructor(JSContext* cx, JSObject* obj, + bool* result) { + if (obj->is<WrapperObject>()) { + obj = CheckedUnwrapDynamic(obj, cx); + if (!obj) { + ReportAccessDenied(cx); + return false; + } + } + + *result = + IsArrayConstructor(obj) && obj->as<JSFunction>().realm() != cx->realm(); + return true; +} + +static MOZ_ALWAYS_INLINE bool IsArraySpecies(JSContext* cx, + HandleObject origArray) { + if (MOZ_UNLIKELY(origArray->is<ProxyObject>())) { + if (origArray->getClass()->isDOMClass()) { +#ifdef DEBUG + // We assume DOM proxies never return true for IsArray. + IsArrayAnswer answer; + MOZ_ASSERT(Proxy::isArray(cx, origArray, &answer)); + MOZ_ASSERT(answer == IsArrayAnswer::NotArray); +#endif + return true; + } + return false; + } + + // 9.4.2.3 Step 4. Non-array objects always use the default constructor. + if (!origArray->is<ArrayObject>()) { + return true; + } + + if (cx->realm()->arraySpeciesLookup.tryOptimizeArray( + cx, &origArray->as<ArrayObject>())) { + return true; + } + + Value ctor; + if (!GetPropertyPure(cx, origArray, NameToId(cx->names().constructor), + &ctor)) { + return false; + } + + if (!IsArrayConstructor(ctor)) { + return ctor.isUndefined(); + } + + // 9.4.2.3 Step 6.c. Use the current realm's constructor if |ctor| is a + // cross-realm Array constructor. + if (cx->realm() != ctor.toObject().as<JSFunction>().realm()) { + return true; + } + + jsid speciesId = PropertyKey::Symbol(cx->wellKnownSymbols().species); + JSFunction* getter; + if (!GetGetterPure(cx, &ctor.toObject(), speciesId, &getter)) { + return false; + } + + if (!getter) { + return false; + } + + return IsSelfHostedFunctionWithName(getter, cx->names().ArraySpecies); +} + +static bool ArraySpeciesCreate(JSContext* cx, HandleObject origArray, + uint64_t length, MutableHandleObject arr) { + MOZ_ASSERT(length < DOUBLE_INTEGRAL_PRECISION_LIMIT); + + FixedInvokeArgs<2> args(cx); + + args[0].setObject(*origArray); + args[1].set(NumberValue(length)); + + RootedValue rval(cx); + if (!CallSelfHostedFunction(cx, cx->names().ArraySpeciesCreate, + UndefinedHandleValue, args, &rval)) { + return false; + } + + MOZ_ASSERT(rval.isObject()); + arr.set(&rval.toObject()); + return true; +} + +JSString* js::ArrayToSource(JSContext* cx, HandleObject obj) { + AutoCycleDetector detector(cx, obj); + if (!detector.init()) { + return nullptr; + } + + JSStringBuilder sb(cx); + + if (detector.foundCycle()) { + if (!sb.append("[]")) { + return nullptr; + } + return sb.finishString(); + } + + if (!sb.append('[')) { + return nullptr; + } + + uint64_t length; + if (!GetLengthPropertyInlined(cx, obj, &length)) { + return nullptr; + } + + RootedValue elt(cx); + for (uint64_t index = 0; index < length; index++) { + bool hole; + if (!CheckForInterrupt(cx) || + !HasAndGetElement(cx, obj, index, &hole, &elt)) { + return nullptr; + } + + /* Get element's character string. */ + JSString* str; + if (hole) { + str = cx->runtime()->emptyString; + } else { + str = ValueToSource(cx, elt); + if (!str) { + return nullptr; + } + } + + /* Append element to buffer. */ + if (!sb.append(str)) { + return nullptr; + } + if (index + 1 != length) { + if (!sb.append(", ")) { + return nullptr; + } + } else if (hole) { + if (!sb.append(',')) { + return nullptr; + } + } + } + + /* Finalize the buffer. */ + if (!sb.append(']')) { + return nullptr; + } + + return sb.finishString(); +} + +static bool array_toSource(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "toSource"); + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.thisv().isObject()) { + ReportIncompatible(cx, args); + return false; + } + + Rooted<JSObject*> obj(cx, &args.thisv().toObject()); + + JSString* str = ArrayToSource(cx, obj); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +template <typename SeparatorOp> +static bool ArrayJoinDenseKernel(JSContext* cx, SeparatorOp sepOp, + Handle<NativeObject*> obj, uint64_t length, + StringBuffer& sb, uint32_t* numProcessed) { + // This loop handles all elements up to initializedLength. If + // length > initLength we rely on the second loop to add the + // other elements. + MOZ_ASSERT(*numProcessed == 0); + uint64_t initLength = + std::min<uint64_t>(obj->getDenseInitializedLength(), length); + MOZ_ASSERT(initLength <= UINT32_MAX, + "initialized length shouldn't exceed UINT32_MAX"); + uint32_t initLengthClamped = uint32_t(initLength); + while (*numProcessed < initLengthClamped) { + if (!CheckForInterrupt(cx)) { + return false; + } + + // Step 7.b. + Value elem = obj->getDenseElement(*numProcessed); + + // Steps 7.c-d. + if (elem.isString()) { + if (!sb.append(elem.toString())) { + return false; + } + } else if (elem.isNumber()) { + if (!NumberValueToStringBuffer(elem, sb)) { + return false; + } + } else if (elem.isBoolean()) { + if (!BooleanToStringBuffer(elem.toBoolean(), sb)) { + return false; + } + } else if (elem.isObject() || elem.isSymbol()) { + /* + * Object stringifying could modify the initialized length or make + * the array sparse. Delegate it to a separate loop to keep this + * one tight. + * + * Symbol stringifying is a TypeError, so into the slow path + * with those as well. + */ + break; + } else if (elem.isBigInt()) { + // ToString(bigint) doesn't access bigint.toString or + // anything like that, so it can't mutate the array we're + // walking through, so it *could* be handled here. We don't + // do so yet for reasons of initial-implementation economy. + break; + } else { + MOZ_ASSERT(elem.isMagic(JS_ELEMENTS_HOLE) || elem.isNullOrUndefined()); + } + + // Steps 7.a, 7.e. + if (++(*numProcessed) != length && !sepOp(sb)) { + return false; + } + } + + return true; +} + +template <typename SeparatorOp> +static bool ArrayJoinKernel(JSContext* cx, SeparatorOp sepOp, HandleObject obj, + uint64_t length, StringBuffer& sb) { + // Step 6. + uint32_t numProcessed = 0; + + if (IsPackedArrayOrNoExtraIndexedProperties(obj, length)) { + if (!ArrayJoinDenseKernel<SeparatorOp>(cx, sepOp, obj.as<NativeObject>(), + length, sb, &numProcessed)) { + return false; + } + } + + // Step 7. + if (numProcessed != length) { + RootedValue v(cx); + for (uint64_t i = numProcessed; i < length;) { + if (!CheckForInterrupt(cx)) { + return false; + } + + // Step 7.b. + if (!GetArrayElement(cx, obj, i, &v)) { + return false; + } + + // Steps 7.c-d. + if (!v.isNullOrUndefined()) { + if (!ValueToStringBuffer(cx, v, sb)) { + return false; + } + } + + // Steps 7.a, 7.e. + if (++i != length && !sepOp(sb)) { + return false; + } + } + } + + return true; +} + +// ES2017 draft rev 1b0184bc17fc09a8ddcf4aeec9b6d9fcac4eafce +// 22.1.3.13 Array.prototype.join ( separator ) +bool js::array_join(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "join"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + AutoCycleDetector detector(cx, obj); + if (!detector.init()) { + return false; + } + + if (detector.foundCycle()) { + args.rval().setString(cx->names().empty); + return true; + } + + // Step 2. + uint64_t length; + if (!GetLengthPropertyInlined(cx, obj, &length)) { + return false; + } + + // Steps 3-4. + Rooted<JSLinearString*> sepstr(cx); + if (args.hasDefined(0)) { + JSString* s = ToString<CanGC>(cx, args[0]); + if (!s) { + return false; + } + sepstr = s->ensureLinear(cx); + if (!sepstr) { + return false; + } + } else { + sepstr = cx->names().comma; + } + + // Steps 5-8 (When the length is zero, directly return the empty string). + if (length == 0) { + args.rval().setString(cx->emptyString()); + return true; + } + + // An optimized version of a special case of steps 5-8: when length==1 and + // the 0th element is a string, ToString() of that element is a no-op and + // so it can be immediately returned as the result. + if (length == 1 && obj->is<NativeObject>()) { + NativeObject* nobj = &obj->as<NativeObject>(); + if (nobj->getDenseInitializedLength() == 1) { + Value elem0 = nobj->getDenseElement(0); + if (elem0.isString()) { + args.rval().set(elem0); + return true; + } + } + } + + // Step 5. + JSStringBuilder sb(cx); + if (sepstr->hasTwoByteChars() && !sb.ensureTwoByteChars()) { + return false; + } + + // The separator will be added |length - 1| times, reserve space for that + // so that we don't have to unnecessarily grow the buffer. + size_t seplen = sepstr->length(); + if (seplen > 0) { + if (length > UINT32_MAX) { + ReportAllocationOverflow(cx); + return false; + } + CheckedInt<uint32_t> res = + CheckedInt<uint32_t>(seplen) * (uint32_t(length) - 1); + if (!res.isValid()) { + ReportAllocationOverflow(cx); + return false; + } + + if (!sb.reserve(res.value())) { + return false; + } + } + + // Various optimized versions of steps 6-7. + if (seplen == 0) { + auto sepOp = [](StringBuffer&) { return true; }; + if (!ArrayJoinKernel(cx, sepOp, obj, length, sb)) { + return false; + } + } else if (seplen == 1) { + char16_t c = sepstr->latin1OrTwoByteChar(0); + if (c <= JSString::MAX_LATIN1_CHAR) { + Latin1Char l1char = Latin1Char(c); + auto sepOp = [l1char](StringBuffer& sb) { return sb.append(l1char); }; + if (!ArrayJoinKernel(cx, sepOp, obj, length, sb)) { + return false; + } + } else { + auto sepOp = [c](StringBuffer& sb) { return sb.append(c); }; + if (!ArrayJoinKernel(cx, sepOp, obj, length, sb)) { + return false; + } + } + } else { + Handle<JSLinearString*> sepHandle = sepstr; + auto sepOp = [sepHandle](StringBuffer& sb) { return sb.append(sepHandle); }; + if (!ArrayJoinKernel(cx, sepOp, obj, length, sb)) { + return false; + } + } + + // Step 8. + JSString* str = sb.finishString(); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +// ES2017 draft rev f8a9be8ea4bd97237d176907a1e3080dce20c68f +// 22.1.3.27 Array.prototype.toLocaleString ([ reserved1 [ , reserved2 ] ]) +// ES2017 Intl draft rev 78bbe7d1095f5ff3760ac4017ed366026e4cb276 +// 13.4.1 Array.prototype.toLocaleString ([ locales [ , options ]]) +static bool array_toLocaleString(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", + "toLocaleString"); + + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1 + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + // Avoid calling into self-hosted code if the array is empty. + if (obj->is<ArrayObject>() && obj->as<ArrayObject>().length() == 0) { + args.rval().setString(cx->names().empty); + return true; + } + + AutoCycleDetector detector(cx, obj); + if (!detector.init()) { + return false; + } + + if (detector.foundCycle()) { + args.rval().setString(cx->names().empty); + return true; + } + + FixedInvokeArgs<2> args2(cx); + + args2[0].set(args.get(0)); + args2[1].set(args.get(1)); + + // Steps 2-10. + RootedValue thisv(cx, ObjectValue(*obj)); + return CallSelfHostedFunction(cx, cx->names().ArrayToLocaleString, thisv, + args2, args.rval()); +} + +/* vector must point to rooted memory. */ +static bool SetArrayElements(JSContext* cx, HandleObject obj, uint64_t start, + uint32_t count, const Value* vector) { + MOZ_ASSERT(count <= MAX_ARRAY_INDEX); + MOZ_ASSERT(start + count < uint64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)); + + if (count == 0) { + return true; + } + + if (!ObjectMayHaveExtraIndexedProperties(obj) && start <= UINT32_MAX) { + NativeObject* nobj = &obj->as<NativeObject>(); + DenseElementResult result = + nobj->setOrExtendDenseElements(cx, uint32_t(start), vector, count); + if (result != DenseElementResult::Incomplete) { + return result == DenseElementResult::Success; + } + } + + RootedId id(cx); + const Value* end = vector + count; + while (vector < end) { + if (!CheckForInterrupt(cx)) { + return false; + } + + if (!ToId(cx, start++, &id)) { + return false; + } + + if (!SetProperty(cx, obj, id, HandleValue::fromMarkedLocation(vector++))) { + return false; + } + } + + return true; +} + +static DenseElementResult ArrayReverseDenseKernel(JSContext* cx, + Handle<NativeObject*> obj, + uint32_t length) { + MOZ_ASSERT(length > 1); + + // If there are no elements, we're done. + if (obj->getDenseInitializedLength() == 0) { + return DenseElementResult::Success; + } + + if (!obj->isExtensible()) { + return DenseElementResult::Incomplete; + } + + if (!IsPackedArray(obj)) { + /* + * It's actually surprisingly complicated to reverse an array due + * to the orthogonality of array length and array capacity while + * handling leading and trailing holes correctly. Reversing seems + * less likely to be a common operation than other array + * mass-mutation methods, so for now just take a probably-small + * memory hit (in the absence of too many holes in the array at + * its start) and ensure that the capacity is sufficient to hold + * all the elements in the array if it were full. + */ + DenseElementResult result = obj->ensureDenseElements(cx, length, 0); + if (result != DenseElementResult::Success) { + return result; + } + + /* Fill out the array's initialized length to its proper length. */ + obj->ensureDenseInitializedLength(length, 0); + } + + if (!obj->denseElementsMaybeInIteration() && + !cx->zone()->needsIncrementalBarrier()) { + obj->reverseDenseElementsNoPreBarrier(length); + return DenseElementResult::Success; + } + + auto setElementMaybeHole = [](JSContext* cx, Handle<NativeObject*> obj, + uint32_t index, const Value& val) { + if (MOZ_LIKELY(!val.isMagic(JS_ELEMENTS_HOLE))) { + obj->setDenseElement(index, val); + return true; + } + + obj->setDenseElementHole(index); + return SuppressDeletedProperty(cx, obj, PropertyKey::Int(index)); + }; + + RootedValue origlo(cx), orighi(cx); + + uint32_t lo = 0, hi = length - 1; + for (; lo < hi; lo++, hi--) { + origlo = obj->getDenseElement(lo); + orighi = obj->getDenseElement(hi); + if (!setElementMaybeHole(cx, obj, lo, orighi)) { + return DenseElementResult::Failure; + } + if (!setElementMaybeHole(cx, obj, hi, origlo)) { + return DenseElementResult::Failure; + } + } + + return DenseElementResult::Success; +} + +// ES2017 draft rev 1b0184bc17fc09a8ddcf4aeec9b6d9fcac4eafce +// 22.1.3.21 Array.prototype.reverse ( ) +static bool array_reverse(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "reverse"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + // Step 2. + uint64_t len; + if (!GetLengthPropertyInlined(cx, obj, &len)) { + return false; + } + + // An empty array or an array with length 1 is already reversed. + if (len <= 1) { + args.rval().setObject(*obj); + return true; + } + + if (IsPackedArrayOrNoExtraIndexedProperties(obj, len) && len <= UINT32_MAX) { + DenseElementResult result = + ArrayReverseDenseKernel(cx, obj.as<NativeObject>(), uint32_t(len)); + if (result != DenseElementResult::Incomplete) { + /* + * Per ECMA-262, don't update the length of the array, even if the new + * array has trailing holes (and thus the original array began with + * holes). + */ + args.rval().setObject(*obj); + return result == DenseElementResult::Success; + } + } + + // Steps 3-5. + RootedValue lowval(cx), hival(cx); + for (uint64_t i = 0, half = len / 2; i < half; i++) { + bool hole, hole2; + if (!CheckForInterrupt(cx) || + !HasAndGetElement(cx, obj, i, &hole, &lowval) || + !HasAndGetElement(cx, obj, len - i - 1, &hole2, &hival)) { + return false; + } + + if (!hole && !hole2) { + if (!SetArrayElement(cx, obj, i, hival)) { + return false; + } + if (!SetArrayElement(cx, obj, len - i - 1, lowval)) { + return false; + } + } else if (hole && !hole2) { + if (!SetArrayElement(cx, obj, i, hival)) { + return false; + } + if (!DeletePropertyOrThrow(cx, obj, len - i - 1)) { + return false; + } + } else if (!hole && hole2) { + if (!DeletePropertyOrThrow(cx, obj, i)) { + return false; + } + if (!SetArrayElement(cx, obj, len - i - 1, lowval)) { + return false; + } + } else { + // No action required. + } + } + + // Step 6. + args.rval().setObject(*obj); + return true; +} + +static inline bool CompareStringValues(JSContext* cx, const Value& a, + const Value& b, bool* lessOrEqualp) { + if (!CheckForInterrupt(cx)) { + return false; + } + + JSString* astr = a.toString(); + JSString* bstr = b.toString(); + int32_t result; + if (!CompareStrings(cx, astr, bstr, &result)) { + return false; + } + + *lessOrEqualp = (result <= 0); + return true; +} + +static const uint64_t powersOf10[] = { + 1, 10, 100, 1000, 10000, 100000, + 1000000, 10000000, 100000000, 1000000000, 1000000000000ULL}; + +static inline unsigned NumDigitsBase10(uint32_t n) { + /* + * This is just floor_log10(n) + 1 + * Algorithm taken from + * http://graphics.stanford.edu/~seander/bithacks.html#IntegerLog10 + */ + uint32_t log2 = CeilingLog2(n); + uint32_t t = log2 * 1233 >> 12; + return t - (n < powersOf10[t]) + 1; +} + +static inline bool CompareLexicographicInt32(const Value& a, const Value& b, + bool* lessOrEqualp) { + int32_t aint = a.toInt32(); + int32_t bint = b.toInt32(); + + /* + * If both numbers are equal ... trivial + * If only one of both is negative --> arithmetic comparison as char code + * of '-' is always less than any other digit + * If both numbers are negative convert them to positive and continue + * handling ... + */ + if (aint == bint) { + *lessOrEqualp = true; + } else if ((aint < 0) && (bint >= 0)) { + *lessOrEqualp = true; + } else if ((aint >= 0) && (bint < 0)) { + *lessOrEqualp = false; + } else { + uint32_t auint = Abs(aint); + uint32_t buint = Abs(bint); + + /* + * ... get number of digits of both integers. + * If they have the same number of digits --> arithmetic comparison. + * If digits_a > digits_b: a < b*10e(digits_a - digits_b). + * If digits_b > digits_a: a*10e(digits_b - digits_a) <= b. + */ + unsigned digitsa = NumDigitsBase10(auint); + unsigned digitsb = NumDigitsBase10(buint); + if (digitsa == digitsb) { + *lessOrEqualp = (auint <= buint); + } else if (digitsa > digitsb) { + MOZ_ASSERT((digitsa - digitsb) < std::size(powersOf10)); + *lessOrEqualp = + (uint64_t(auint) < uint64_t(buint) * powersOf10[digitsa - digitsb]); + } else { /* if (digitsb > digitsa) */ + MOZ_ASSERT((digitsb - digitsa) < std::size(powersOf10)); + *lessOrEqualp = + (uint64_t(auint) * powersOf10[digitsb - digitsa] <= uint64_t(buint)); + } + } + + return true; +} + +template <typename Char1, typename Char2> +static inline bool CompareSubStringValues(JSContext* cx, const Char1* s1, + size_t len1, const Char2* s2, + size_t len2, bool* lessOrEqualp) { + if (!CheckForInterrupt(cx)) { + return false; + } + + if (!s1 || !s2) { + return false; + } + + int32_t result = CompareChars(s1, len1, s2, len2); + *lessOrEqualp = (result <= 0); + return true; +} + +namespace { + +struct SortComparatorStrings { + JSContext* const cx; + + explicit SortComparatorStrings(JSContext* cx) : cx(cx) {} + + bool operator()(const Value& a, const Value& b, bool* lessOrEqualp) { + return CompareStringValues(cx, a, b, lessOrEqualp); + } +}; + +struct SortComparatorLexicographicInt32 { + bool operator()(const Value& a, const Value& b, bool* lessOrEqualp) { + return CompareLexicographicInt32(a, b, lessOrEqualp); + } +}; + +struct StringifiedElement { + size_t charsBegin; + size_t charsEnd; + size_t elementIndex; +}; + +struct SortComparatorStringifiedElements { + JSContext* const cx; + const StringBuffer& sb; + + SortComparatorStringifiedElements(JSContext* cx, const StringBuffer& sb) + : cx(cx), sb(sb) {} + + bool operator()(const StringifiedElement& a, const StringifiedElement& b, + bool* lessOrEqualp) { + size_t lenA = a.charsEnd - a.charsBegin; + size_t lenB = b.charsEnd - b.charsBegin; + + if (sb.isUnderlyingBufferLatin1()) { + return CompareSubStringValues(cx, sb.rawLatin1Begin() + a.charsBegin, + lenA, sb.rawLatin1Begin() + b.charsBegin, + lenB, lessOrEqualp); + } + + return CompareSubStringValues(cx, sb.rawTwoByteBegin() + a.charsBegin, lenA, + sb.rawTwoByteBegin() + b.charsBegin, lenB, + lessOrEqualp); + } +}; + +struct NumericElement { + double dv; + size_t elementIndex; +}; + +static bool ComparatorNumericLeftMinusRight(const NumericElement& a, + const NumericElement& b, + bool* lessOrEqualp) { + *lessOrEqualp = std::isunordered(a.dv, b.dv) || (a.dv <= b.dv); + return true; +} + +static bool ComparatorNumericRightMinusLeft(const NumericElement& a, + const NumericElement& b, + bool* lessOrEqualp) { + *lessOrEqualp = std::isunordered(a.dv, b.dv) || (b.dv <= a.dv); + return true; +} + +using ComparatorNumeric = bool (*)(const NumericElement&, const NumericElement&, + bool*); + +static const ComparatorNumeric SortComparatorNumerics[] = { + nullptr, nullptr, ComparatorNumericLeftMinusRight, + ComparatorNumericRightMinusLeft}; + +static bool ComparatorInt32LeftMinusRight(const Value& a, const Value& b, + bool* lessOrEqualp) { + *lessOrEqualp = (a.toInt32() <= b.toInt32()); + return true; +} + +static bool ComparatorInt32RightMinusLeft(const Value& a, const Value& b, + bool* lessOrEqualp) { + *lessOrEqualp = (b.toInt32() <= a.toInt32()); + return true; +} + +using ComparatorInt32 = bool (*)(const Value&, const Value&, bool*); + +static const ComparatorInt32 SortComparatorInt32s[] = { + nullptr, nullptr, ComparatorInt32LeftMinusRight, + ComparatorInt32RightMinusLeft}; + +// Note: Values for this enum must match up with SortComparatorNumerics +// and SortComparatorInt32s. +enum ComparatorMatchResult { + Match_Failure = 0, + Match_None, + Match_LeftMinusRight, + Match_RightMinusLeft +}; + +} // namespace + +/* + * Specialize behavior for comparator functions with particular common bytecode + * patterns: namely, |return x - y| and |return y - x|. + */ +static ComparatorMatchResult MatchNumericComparator(JSContext* cx, + JSObject* obj) { + if (!obj->is<JSFunction>()) { + return Match_None; + } + + RootedFunction fun(cx, &obj->as<JSFunction>()); + if (!fun->isInterpreted() || fun->isClassConstructor()) { + return Match_None; + } + + JSScript* script = JSFunction::getOrCreateScript(cx, fun); + if (!script) { + return Match_Failure; + } + + jsbytecode* pc = script->code(); + + uint16_t arg0, arg1; + if (JSOp(*pc) != JSOp::GetArg) { + return Match_None; + } + arg0 = GET_ARGNO(pc); + pc += JSOpLength_GetArg; + + if (JSOp(*pc) != JSOp::GetArg) { + return Match_None; + } + arg1 = GET_ARGNO(pc); + pc += JSOpLength_GetArg; + + if (JSOp(*pc) != JSOp::Sub) { + return Match_None; + } + pc += JSOpLength_Sub; + + if (JSOp(*pc) != JSOp::Return) { + return Match_None; + } + + if (arg0 == 0 && arg1 == 1) { + return Match_LeftMinusRight; + } + + if (arg0 == 1 && arg1 == 0) { + return Match_RightMinusLeft; + } + + return Match_None; +} + +template <typename K, typename C> +static inline bool MergeSortByKey(K keys, size_t len, K scratch, C comparator, + MutableHandle<GCVector<Value>> vec) { + MOZ_ASSERT(vec.length() >= len); + + /* Sort keys. */ + if (!MergeSort(keys, len, scratch, comparator)) { + return false; + } + + /* + * Reorder vec by keys in-place, going element by element. When an out-of- + * place element is encountered, move that element to its proper position, + * displacing whatever element was at *that* point to its proper position, + * and so on until an element must be moved to the current position. + * + * At each outer iteration all elements up to |i| are sorted. If + * necessary each inner iteration moves some number of unsorted elements + * (including |i|) directly to sorted position. Thus on completion |*vec| + * is sorted, and out-of-position elements have moved once. Complexity is + * Θ(len) + O(len) == O(2*len), with each element visited at most twice. + */ + for (size_t i = 0; i < len; i++) { + size_t j = keys[i].elementIndex; + if (i == j) { + continue; // fixed point + } + + MOZ_ASSERT(j > i, "Everything less than |i| should be in the right place!"); + Value tv = vec[j]; + do { + size_t k = keys[j].elementIndex; + keys[j].elementIndex = j; + vec[j].set(vec[k]); + j = k; + } while (j != i); + + // We could assert the loop invariant that |i == keys[i].elementIndex| + // here if we synced |keys[i].elementIndex|. But doing so would render + // the assertion vacuous, so don't bother, even in debug builds. + vec[i].set(tv); + } + + return true; +} + +/* + * Sort Values as strings. + * + * To minimize #conversions, SortLexicographically() first converts all Values + * to strings at once, then sorts the elements by these cached strings. + */ +static bool SortLexicographically(JSContext* cx, + MutableHandle<GCVector<Value>> vec, + size_t len) { + MOZ_ASSERT(vec.length() >= len); + + StringBuffer sb(cx); + Vector<StringifiedElement, 0, TempAllocPolicy> strElements(cx); + + /* MergeSort uses the upper half as scratch space. */ + if (!strElements.resize(2 * len)) { + return false; + } + + /* Convert Values to strings. */ + size_t cursor = 0; + for (size_t i = 0; i < len; i++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + if (!ValueToStringBuffer(cx, vec[i], sb)) { + return false; + } + + strElements[i] = {cursor, sb.length(), i}; + cursor = sb.length(); + } + + /* Sort Values in vec alphabetically. */ + return MergeSortByKey(strElements.begin(), len, strElements.begin() + len, + SortComparatorStringifiedElements(cx, sb), vec); +} + +/* + * Sort Values as numbers. + * + * To minimize #conversions, SortNumerically first converts all Values to + * numerics at once, then sorts the elements by these cached numerics. + */ +static bool SortNumerically(JSContext* cx, MutableHandle<GCVector<Value>> vec, + size_t len, ComparatorMatchResult comp) { + MOZ_ASSERT(vec.length() >= len); + + Vector<NumericElement, 0, TempAllocPolicy> numElements(cx); + + /* MergeSort uses the upper half as scratch space. */ + if (!numElements.resize(2 * len)) { + return false; + } + + /* Convert Values to numerics. */ + for (size_t i = 0; i < len; i++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + double dv; + if (!ToNumber(cx, vec[i], &dv)) { + return false; + } + + numElements[i] = {dv, i}; + } + + /* Sort Values in vec numerically. */ + return MergeSortByKey(numElements.begin(), len, numElements.begin() + len, + SortComparatorNumerics[comp], vec); +} + +static bool FillWithUndefined(JSContext* cx, HandleObject obj, uint32_t start, + uint32_t count) { + MOZ_ASSERT(start < start + count, + "count > 0 and start + count doesn't overflow"); + + do { + if (ObjectMayHaveExtraIndexedProperties(obj)) { + break; + } + + NativeObject* nobj = &obj->as<NativeObject>(); + if (!nobj->isExtensible()) { + break; + } + + if (obj->is<ArrayObject>() && !obj->as<ArrayObject>().lengthIsWritable() && + start + count >= obj->as<ArrayObject>().length()) { + break; + } + + DenseElementResult result = nobj->ensureDenseElements(cx, start, count); + if (result != DenseElementResult::Success) { + if (result == DenseElementResult::Failure) { + return false; + } + MOZ_ASSERT(result == DenseElementResult::Incomplete); + break; + } + + if (obj->is<ArrayObject>() && + start + count >= obj->as<ArrayObject>().length()) { + obj->as<ArrayObject>().setLength(start + count); + } + + for (uint32_t i = 0; i < count; i++) { + nobj->setDenseElement(start + i, UndefinedHandleValue); + } + + return true; + } while (false); + + for (uint32_t i = 0; i < count; i++) { + if (!CheckForInterrupt(cx) || + !SetArrayElement(cx, obj, start + i, UndefinedHandleValue)) { + return false; + } + } + + return true; +} + +static bool ArrayNativeSortImpl(JSContext* cx, Handle<JSObject*> obj, + Handle<Value> fval, ComparatorMatchResult comp); + +bool js::intrinsic_ArrayNativeSort(JSContext* cx, unsigned argc, Value* vp) { + // This function is called from the self-hosted Array.prototype.sort + // implementation. It returns |true| if the array was sorted, otherwise it + // returns |false| to notify the self-hosted code to perform the sorting. + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + HandleValue fval = args[0]; + MOZ_ASSERT(fval.isUndefined() || IsCallable(fval)); + + ComparatorMatchResult comp; + if (fval.isObject()) { + comp = MatchNumericComparator(cx, &fval.toObject()); + if (comp == Match_Failure) { + return false; + } + + if (comp == Match_None) { + // Non-optimized user supplied comparators perform much better when + // called from within a self-hosted sorting function. + args.rval().setBoolean(false); + return true; + } + } else { + comp = Match_None; + } + + Rooted<JSObject*> obj(cx, &args.thisv().toObject()); + + if (!ArrayNativeSortImpl(cx, obj, fval, comp)) { + return false; + } + + args.rval().setBoolean(true); + return true; +} + +static bool ArrayNativeSortImpl(JSContext* cx, Handle<JSObject*> obj, + Handle<Value> fval, + ComparatorMatchResult comp) { + uint64_t length; + if (!GetLengthPropertyInlined(cx, obj, &length)) { + return false; + } + if (length < 2) { + /* [] and [a] remain unchanged when sorted. */ + return true; + } + + if (length > UINT32_MAX) { + ReportAllocationOverflow(cx); + return false; + } + uint32_t len = uint32_t(length); + + /* + * We need a temporary array of 2 * len Value to hold the array elements + * and the scratch space for merge sort. Check that its size does not + * overflow size_t, which would allow for indexing beyond the end of the + * malloc'd vector. + */ +#if JS_BITS_PER_WORD == 32 + if (size_t(len) > size_t(-1) / (2 * sizeof(Value))) { + ReportAllocationOverflow(cx); + return false; + } +#endif + + size_t n, undefs; + { + Rooted<GCVector<Value>> vec(cx, GCVector<Value>(cx)); + if (!vec.reserve(2 * size_t(len))) { + return false; + } + + /* + * By ECMA 262, 15.4.4.11, a property that does not exist (which we + * call a "hole") is always greater than an existing property with + * value undefined and that is always greater than any other property. + * Thus to sort holes and undefs we simply count them, sort the rest + * of elements, append undefs after them and then make holes after + * undefs. + */ + undefs = 0; + bool allStrings = true; + bool allInts = true; + RootedValue v(cx); + if (IsPackedArray(obj)) { + Handle<ArrayObject*> array = obj.as<ArrayObject>(); + + for (uint32_t i = 0; i < len; i++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + v.set(array->getDenseElement(i)); + MOZ_ASSERT(!v.isMagic(JS_ELEMENTS_HOLE)); + if (v.isUndefined()) { + ++undefs; + continue; + } + vec.infallibleAppend(v); + allStrings = allStrings && v.isString(); + allInts = allInts && v.isInt32(); + } + } else { + for (uint32_t i = 0; i < len; i++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + bool hole; + if (!HasAndGetElement(cx, obj, i, &hole, &v)) { + return false; + } + if (hole) { + continue; + } + if (v.isUndefined()) { + ++undefs; + continue; + } + vec.infallibleAppend(v); + allStrings = allStrings && v.isString(); + allInts = allInts && v.isInt32(); + } + } + + /* + * If the array only contains holes, we're done. But if it contains + * undefs, those must be sorted to the front of the array. + */ + n = vec.length(); + if (n == 0 && undefs == 0) { + return true; + } + + /* Here len == n + undefs + number_of_holes. */ + if (comp == Match_None) { + /* + * Sort using the default comparator converting all elements to + * strings. + */ + if (allStrings) { + MOZ_ALWAYS_TRUE(vec.resize(n * 2)); + if (!MergeSort(vec.begin(), n, vec.begin() + n, + SortComparatorStrings(cx))) { + return false; + } + } else if (allInts) { + MOZ_ALWAYS_TRUE(vec.resize(n * 2)); + if (!MergeSort(vec.begin(), n, vec.begin() + n, + SortComparatorLexicographicInt32())) { + return false; + } + } else { + if (!SortLexicographically(cx, &vec, n)) { + return false; + } + } + } else { + if (allInts) { + MOZ_ALWAYS_TRUE(vec.resize(n * 2)); + if (!MergeSort(vec.begin(), n, vec.begin() + n, + SortComparatorInt32s[comp])) { + return false; + } + } else { + if (!SortNumerically(cx, &vec, n, comp)) { + return false; + } + } + } + + if (!SetArrayElements(cx, obj, 0, uint32_t(n), vec.begin())) { + return false; + } + } + + /* Set undefs that sorted after the rest of elements. */ + if (undefs > 0) { + if (!FillWithUndefined(cx, obj, n, undefs)) { + return false; + } + n += undefs; + } + + /* Re-create any holes that sorted to the end of the array. */ + for (uint32_t i = n; i < len; i++) { + if (!CheckForInterrupt(cx) || !DeletePropertyOrThrow(cx, obj, i)) { + return false; + } + } + return true; +} + +bool js::NewbornArrayPush(JSContext* cx, HandleObject obj, const Value& v) { + Handle<ArrayObject*> arr = obj.as<ArrayObject>(); + + MOZ_ASSERT(!v.isMagic()); + MOZ_ASSERT(arr->lengthIsWritable()); + + uint32_t length = arr->length(); + MOZ_ASSERT(length <= arr->getDenseCapacity()); + + if (!arr->ensureElements(cx, length + 1)) { + return false; + } + + arr->setDenseInitializedLength(length + 1); + arr->setLength(length + 1); + arr->initDenseElement(length, v); + return true; +} + +// ES2017 draft rev 1b0184bc17fc09a8ddcf4aeec9b6d9fcac4eafce +// 22.1.3.18 Array.prototype.push ( ...items ) +static bool array_push(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "push"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + // Step 2. + uint64_t length; + if (!GetLengthPropertyInlined(cx, obj, &length)) { + return false; + } + + if (!ObjectMayHaveExtraIndexedProperties(obj) && length <= UINT32_MAX) { + DenseElementResult result = + obj->as<NativeObject>().setOrExtendDenseElements( + cx, uint32_t(length), args.array(), args.length()); + if (result != DenseElementResult::Incomplete) { + if (result == DenseElementResult::Failure) { + return false; + } + + uint32_t newlength = uint32_t(length) + args.length(); + args.rval().setNumber(newlength); + + // setOrExtendDenseElements takes care of updating the length for + // arrays. Handle updates to the length of non-arrays here. + if (!obj->is<ArrayObject>()) { + MOZ_ASSERT(obj->is<NativeObject>()); + return SetLengthProperty(cx, obj, newlength); + } + + return true; + } + } + + // Step 5. + uint64_t newlength = length + args.length(); + if (newlength >= uint64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TOO_LONG_ARRAY); + return false; + } + + // Steps 3-6. + if (!SetArrayElements(cx, obj, length, args.length(), args.array())) { + return false; + } + + // Steps 7-8. + args.rval().setNumber(double(newlength)); + return SetLengthProperty(cx, obj, newlength); +} + +// ES2017 draft rev 1b0184bc17fc09a8ddcf4aeec9b6d9fcac4eafce +// 22.1.3.17 Array.prototype.pop ( ) +bool js::array_pop(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "pop"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + // Step 2. + uint64_t index; + if (!GetLengthPropertyInlined(cx, obj, &index)) { + return false; + } + + // Steps 3-4. + if (index == 0) { + // Step 3.b. + args.rval().setUndefined(); + } else { + // Steps 4.a-b. + index--; + + // Steps 4.c, 4.f. + if (!GetArrayElement(cx, obj, index, args.rval())) { + return false; + } + + // Steps 4.d. + if (!DeletePropertyOrThrow(cx, obj, index)) { + return false; + } + } + + // Steps 3.a, 4.e. + return SetLengthProperty(cx, obj, index); +} + +void js::ArrayShiftMoveElements(ArrayObject* arr) { + AutoUnsafeCallWithABI unsafe; + MOZ_ASSERT(arr->isExtensible()); + MOZ_ASSERT(arr->lengthIsWritable()); + MOZ_ASSERT(IsPackedArray(arr)); + MOZ_ASSERT(!arr->denseElementsHaveMaybeInIterationFlag()); + + size_t initlen = arr->getDenseInitializedLength(); + MOZ_ASSERT(initlen > 0); + + if (!arr->tryShiftDenseElements(1)) { + arr->moveDenseElements(0, 1, initlen - 1); + arr->setDenseInitializedLength(initlen - 1); + } + + MOZ_ASSERT(arr->getDenseInitializedLength() == initlen - 1); + arr->setLength(initlen - 1); +} + +static inline void SetInitializedLength(JSContext* cx, NativeObject* obj, + size_t initlen) { + MOZ_ASSERT(obj->isExtensible()); + + size_t oldInitlen = obj->getDenseInitializedLength(); + obj->setDenseInitializedLength(initlen); + if (initlen < oldInitlen) { + obj->shrinkElements(cx, initlen); + } +} + +static DenseElementResult ArrayShiftDenseKernel(JSContext* cx, HandleObject obj, + MutableHandleValue rval) { + if (!IsPackedArray(obj) && ObjectMayHaveExtraIndexedProperties(obj)) { + return DenseElementResult::Incomplete; + } + + Handle<NativeObject*> nobj = obj.as<NativeObject>(); + if (nobj->denseElementsMaybeInIteration()) { + return DenseElementResult::Incomplete; + } + + if (!nobj->isExtensible()) { + return DenseElementResult::Incomplete; + } + + size_t initlen = nobj->getDenseInitializedLength(); + if (initlen == 0) { + return DenseElementResult::Incomplete; + } + + rval.set(nobj->getDenseElement(0)); + if (rval.isMagic(JS_ELEMENTS_HOLE)) { + rval.setUndefined(); + } + + if (nobj->tryShiftDenseElements(1)) { + return DenseElementResult::Success; + } + + nobj->moveDenseElements(0, 1, initlen - 1); + + SetInitializedLength(cx, nobj, initlen - 1); + return DenseElementResult::Success; +} + +// ES2017 draft rev 1b0184bc17fc09a8ddcf4aeec9b6d9fcac4eafce +// 22.1.3.22 Array.prototype.shift ( ) +static bool array_shift(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "shift"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + // Step 2. + uint64_t len; + if (!GetLengthPropertyInlined(cx, obj, &len)) { + return false; + } + + // Step 3. + if (len == 0) { + // Step 3.a. + if (!SetLengthProperty(cx, obj, uint32_t(0))) { + return false; + } + + // Step 3.b. + args.rval().setUndefined(); + return true; + } + + uint64_t newlen = len - 1; + + /* Fast paths. */ + uint64_t startIndex; + DenseElementResult result = ArrayShiftDenseKernel(cx, obj, args.rval()); + if (result != DenseElementResult::Incomplete) { + if (result == DenseElementResult::Failure) { + return false; + } + + if (len <= UINT32_MAX) { + return SetLengthProperty(cx, obj, newlen); + } + + startIndex = UINT32_MAX - 1; + } else { + // Steps 4, 9. + if (!GetElement(cx, obj, 0, args.rval())) { + return false; + } + + startIndex = 0; + } + + // Steps 5-6. + RootedValue value(cx); + for (uint64_t i = startIndex; i < newlen; i++) { + if (!CheckForInterrupt(cx)) { + return false; + } + bool hole; + if (!HasAndGetElement(cx, obj, i + 1, &hole, &value)) { + return false; + } + if (hole) { + if (!DeletePropertyOrThrow(cx, obj, i)) { + return false; + } + } else { + if (!SetArrayElement(cx, obj, i, value)) { + return false; + } + } + } + + // Step 7. + if (!DeletePropertyOrThrow(cx, obj, newlen)) { + return false; + } + + // Step 8. + return SetLengthProperty(cx, obj, newlen); +} + +// ES2017 draft rev 1b0184bc17fc09a8ddcf4aeec9b6d9fcac4eafce +// 22.1.3.29 Array.prototype.unshift ( ...items ) +static bool array_unshift(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "unshift"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + // Step 2. + uint64_t length; + if (!GetLengthPropertyInlined(cx, obj, &length)) { + return false; + } + + // Steps 3-4. + if (args.length() > 0) { + bool optimized = false; + do { + if (length > UINT32_MAX) { + break; + } + if (ObjectMayHaveExtraIndexedProperties(obj)) { + break; + } + NativeObject* nobj = &obj->as<NativeObject>(); + if (nobj->denseElementsMaybeInIteration()) { + break; + } + if (!nobj->isExtensible()) { + break; + } + if (nobj->is<ArrayObject>() && + !nobj->as<ArrayObject>().lengthIsWritable()) { + break; + } + if (!nobj->tryUnshiftDenseElements(args.length())) { + DenseElementResult result = + nobj->ensureDenseElements(cx, uint32_t(length), args.length()); + if (result != DenseElementResult::Success) { + if (result == DenseElementResult::Failure) { + return false; + } + MOZ_ASSERT(result == DenseElementResult::Incomplete); + break; + } + if (length > 0) { + nobj->moveDenseElements(args.length(), 0, uint32_t(length)); + } + } + for (uint32_t i = 0; i < args.length(); i++) { + nobj->setDenseElement(i, args[i]); + } + optimized = true; + } while (false); + + if (!optimized) { + if (length > 0) { + uint64_t last = length; + uint64_t upperIndex = last + args.length(); + + // Step 4.a. + if (upperIndex >= uint64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TOO_LONG_ARRAY); + return false; + } + + // Steps 4.b-c. + RootedValue value(cx); + do { + --last; + --upperIndex; + if (!CheckForInterrupt(cx)) { + return false; + } + bool hole; + if (!HasAndGetElement(cx, obj, last, &hole, &value)) { + return false; + } + if (hole) { + if (!DeletePropertyOrThrow(cx, obj, upperIndex)) { + return false; + } + } else { + if (!SetArrayElement(cx, obj, upperIndex, value)) { + return false; + } + } + } while (last != 0); + } + + // Steps 4.d-f. + /* Copy from args to the bottom of the array. */ + if (!SetArrayElements(cx, obj, 0, args.length(), args.array())) { + return false; + } + } + } + + // Step 5. + uint64_t newlength = length + args.length(); + if (!SetLengthProperty(cx, obj, newlength)) { + return false; + } + + // Step 6. + /* Follow Perl by returning the new array length. */ + args.rval().setNumber(double(newlength)); + return true; +} + +enum class ArrayAccess { Read, Write }; + +/* + * Returns true if this is a dense array whose properties ending at |endIndex| + * (exclusive) may be accessed (get, set, delete) directly through its + * contiguous vector of elements without fear of getters, setters, etc. along + * the prototype chain, or of enumerators requiring notification of + * modifications. + */ +template <ArrayAccess Access> +static bool CanOptimizeForDenseStorage(HandleObject arr, uint64_t endIndex) { + /* If the desired properties overflow dense storage, we can't optimize. */ + if (endIndex > UINT32_MAX) { + return false; + } + + if (Access == ArrayAccess::Read) { + /* + * Dense storage read access is possible for any packed array as long + * as we only access properties within the initialized length. In all + * other cases we need to ensure there are no other indexed properties + * on this object or on the prototype chain. Callers are required to + * clamp the read length, so it doesn't exceed the initialized length. + */ + if (IsPackedArray(arr) && + endIndex <= arr->as<ArrayObject>().getDenseInitializedLength()) { + return true; + } + return !ObjectMayHaveExtraIndexedProperties(arr); + } + + /* There's no optimizing possible if it's not an array. */ + if (!arr->is<ArrayObject>()) { + return false; + } + + /* If the length is non-writable, always pick the slow path */ + if (!arr->as<ArrayObject>().lengthIsWritable()) { + return false; + } + + /* Also pick the slow path if the object is non-extensible. */ + if (!arr->as<ArrayObject>().isExtensible()) { + return false; + } + + /* Also pick the slow path if the object is being iterated over. */ + if (arr->as<ArrayObject>().denseElementsMaybeInIteration()) { + return false; + } + + /* Or we attempt to write to indices outside the initialized length. */ + if (endIndex > arr->as<ArrayObject>().getDenseInitializedLength()) { + return false; + } + + /* + * Now watch out for getters and setters along the prototype chain or in + * other indexed properties on the object. Packed arrays don't have any + * other indexed properties by definition. + */ + return IsPackedArray(arr) || !ObjectMayHaveExtraIndexedProperties(arr); +} + +static ArrayObject* CopyDenseArrayElements(JSContext* cx, + Handle<NativeObject*> obj, + uint32_t begin, uint32_t count) { + size_t initlen = obj->getDenseInitializedLength(); + MOZ_ASSERT(initlen <= UINT32_MAX, + "initialized length shouldn't exceed UINT32_MAX"); + uint32_t newlength = 0; + if (initlen > begin) { + newlength = std::min<uint32_t>(initlen - begin, count); + } + + ArrayObject* narr = NewDenseFullyAllocatedArray(cx, newlength); + if (!narr) { + return nullptr; + } + + MOZ_ASSERT(count >= narr->length()); + narr->setLength(count); + + if (newlength > 0) { + narr->initDenseElements(obj, begin, newlength); + } + + return narr; +} + +static bool CopyArrayElements(JSContext* cx, HandleObject obj, uint64_t begin, + uint64_t count, Handle<ArrayObject*> result) { + MOZ_ASSERT(result->length() == count); + + uint64_t startIndex = 0; + RootedValue value(cx); + + // Use dense storage for new indexed properties where possible. + { + uint32_t index = 0; + uint32_t limit = std::min<uint32_t>(count, PropertyKey::IntMax); + for (; index < limit; index++) { + bool hole; + if (!CheckForInterrupt(cx) || + !HasAndGetElement(cx, obj, begin + index, &hole, &value)) { + return false; + } + + if (!hole) { + DenseElementResult edResult = result->ensureDenseElements(cx, index, 1); + if (edResult != DenseElementResult::Success) { + if (edResult == DenseElementResult::Failure) { + return false; + } + + MOZ_ASSERT(edResult == DenseElementResult::Incomplete); + if (!DefineDataElement(cx, result, index, value)) { + return false; + } + + break; + } + result->setDenseElement(index, value); + } + } + startIndex = index + 1; + } + + // Copy any remaining elements. + for (uint64_t i = startIndex; i < count; i++) { + bool hole; + if (!CheckForInterrupt(cx) || + !HasAndGetElement(cx, obj, begin + i, &hole, &value)) { + return false; + } + + if (!hole && !DefineArrayElement(cx, result, i, value)) { + return false; + } + } + return true; +} + +// Helpers for array_splice_impl() and array_to_spliced() +// +// Initialize variables common to splice() and toSpliced(): +// - GetActualStart() returns the index at which to start deleting elements. +// - GetItemCount() returns the number of new elements being added. +// - GetActualDeleteCount() returns the number of elements being deleted. +static bool GetActualStart(JSContext* cx, HandleValue start, uint64_t len, + uint64_t* result) { + MOZ_ASSERT(len < DOUBLE_INTEGRAL_PRECISION_LIMIT); + + // Steps from proposal: https://github.com/tc39/proposal-change-array-by-copy + // Array.prototype.toSpliced() + + // Step 3. Let relativeStart be ? ToIntegerOrInfinity(start). + double relativeStart; + if (!ToInteger(cx, start, &relativeStart)) { + return false; + } + + // Steps 4-5. If relativeStart is -∞, let actualStart be 0. + // Else if relativeStart < 0, let actualStart be max(len + relativeStart, 0). + if (relativeStart < 0) { + *result = uint64_t(std::max(double(len) + relativeStart, 0.0)); + } else { + // Step 6. Else, let actualStart be min(relativeStart, len). + *result = uint64_t(std::min(relativeStart, double(len))); + } + return true; +} + +static uint32_t GetItemCount(const CallArgs& args) { + if (args.length() < 2) { + return 0; + } + return (args.length() - 2); +} + +static bool GetActualDeleteCount(JSContext* cx, const CallArgs& args, + HandleObject obj, uint64_t len, + uint64_t actualStart, uint32_t insertCount, + uint64_t* actualDeleteCount) { + MOZ_ASSERT(len < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(actualStart <= len); + MOZ_ASSERT(insertCount == GetItemCount(args)); + + // Steps from proposal: https://github.com/tc39/proposal-change-array-by-copy + // Array.prototype.toSpliced() + + if (args.length() < 1) { + // Step 8. If start is not present, then let actualDeleteCount be 0. + *actualDeleteCount = 0; + } else if (args.length() < 2) { + // Step 9. Else if deleteCount is not present, then let actualDeleteCount be + // len - actualStart. + *actualDeleteCount = len - actualStart; + } else { + // Step 10.a. Else, let dc be toIntegerOrInfinity(deleteCount). + double deleteCount; + if (!ToInteger(cx, args.get(1), &deleteCount)) { + return false; + } + + // Step 10.b. Let actualDeleteCount be the result of clamping dc between 0 + // and len - actualStart. + *actualDeleteCount = uint64_t( + std::min(std::max(0.0, deleteCount), double(len - actualStart))); + MOZ_ASSERT(*actualDeleteCount <= len); + + // Step 11. Let newLen be len + insertCount - actualDeleteCount. + // Step 12. If newLen > 2^53 - 1, throw a TypeError exception. + if (len + uint64_t(insertCount) - *actualDeleteCount >= + uint64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TOO_LONG_ARRAY); + return false; + } + } + MOZ_ASSERT(actualStart + *actualDeleteCount <= len); + + return true; +} + +static bool array_splice_impl(JSContext* cx, unsigned argc, Value* vp, + bool returnValueIsUsed) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "splice"); + CallArgs args = CallArgsFromVp(argc, vp); + + /* Step 1. */ + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + /* Step 2. */ + uint64_t len; + if (!GetLengthPropertyInlined(cx, obj, &len)) { + return false; + } + + /* Steps 3-6. */ + /* actualStart is the index after which elements will be + deleted and/or new elements will be added */ + uint64_t actualStart; + if (!GetActualStart(cx, args.get(0), len, &actualStart)) { + return false; + } + + /* Steps 7-10.*/ + /* itemCount is the number of elements being added */ + uint32_t itemCount = GetItemCount(args); + + /* actualDeleteCount is the number of elements being deleted */ + uint64_t actualDeleteCount; + if (!GetActualDeleteCount(cx, args, obj, len, actualStart, itemCount, + &actualDeleteCount)) { + return false; + } + + RootedObject arr(cx); + if (IsArraySpecies(cx, obj)) { + if (actualDeleteCount > UINT32_MAX) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_ARRAY_LENGTH); + return false; + } + uint32_t count = uint32_t(actualDeleteCount); + + if (CanOptimizeForDenseStorage<ArrayAccess::Read>(obj, + actualStart + count)) { + MOZ_ASSERT(actualStart <= UINT32_MAX, + "if actualStart + count <= UINT32_MAX, then actualStart <= " + "UINT32_MAX"); + if (returnValueIsUsed) { + /* Steps 11-13. */ + arr = CopyDenseArrayElements(cx, obj.as<NativeObject>(), + uint32_t(actualStart), count); + if (!arr) { + return false; + } + } + } else { + /* Step 11. */ + arr = NewDenseFullyAllocatedArray(cx, count); + if (!arr) { + return false; + } + + /* Steps 12-13. */ + if (!CopyArrayElements(cx, obj, actualStart, count, + arr.as<ArrayObject>())) { + return false; + } + } + } else { + /* Step 11. */ + if (!ArraySpeciesCreate(cx, obj, actualDeleteCount, &arr)) { + return false; + } + + /* Steps 12-13. */ + RootedValue fromValue(cx); + for (uint64_t k = 0; k < actualDeleteCount; k++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + /* Steps 13.b, 13.c.i. */ + bool hole; + if (!HasAndGetElement(cx, obj, actualStart + k, &hole, &fromValue)) { + return false; + } + + /* Step 13.c. */ + if (!hole) { + /* Step 13.c.ii. */ + if (!DefineArrayElement(cx, arr, k, fromValue)) { + return false; + } + } + } + + /* Step 14. */ + if (!SetLengthProperty(cx, arr, actualDeleteCount)) { + return false; + } + } + + /* Step 15. */ + uint64_t finalLength = len - actualDeleteCount + itemCount; + + if (itemCount < actualDeleteCount) { + /* Step 16: the array is being shrunk. */ + uint64_t sourceIndex = actualStart + actualDeleteCount; + uint64_t targetIndex = actualStart + itemCount; + + if (CanOptimizeForDenseStorage<ArrayAccess::Write>(obj, len)) { + MOZ_ASSERT(sourceIndex <= len && targetIndex <= len && len <= UINT32_MAX, + "sourceIndex and targetIndex are uint32 array indices"); + MOZ_ASSERT(finalLength < len, "finalLength is strictly less than len"); + MOZ_ASSERT(obj->is<NativeObject>()); + + /* Step 16.b. */ + Handle<ArrayObject*> arr = obj.as<ArrayObject>(); + if (targetIndex != 0 || !arr->tryShiftDenseElements(sourceIndex)) { + arr->moveDenseElements(uint32_t(targetIndex), uint32_t(sourceIndex), + uint32_t(len - sourceIndex)); + } + + /* Steps 20. */ + SetInitializedLength(cx, arr, finalLength); + } else { + /* + * This is all very slow if the length is very large. We don't yet + * have the ability to iterate in sorted order, so we just do the + * pessimistic thing and let CheckForInterrupt handle the + * fallout. + */ + + /* Step 16. */ + RootedValue fromValue(cx); + for (uint64_t from = sourceIndex, to = targetIndex; from < len; + from++, to++) { + /* Steps 15.b.i-ii (implicit). */ + + if (!CheckForInterrupt(cx)) { + return false; + } + + /* Steps 16.b.iii-v */ + bool hole; + if (!HasAndGetElement(cx, obj, from, &hole, &fromValue)) { + return false; + } + + if (hole) { + if (!DeletePropertyOrThrow(cx, obj, to)) { + return false; + } + } else { + if (!SetArrayElement(cx, obj, to, fromValue)) { + return false; + } + } + } + + /* Step 16d. */ + if (!DeletePropertiesOrThrow(cx, obj, len, finalLength)) { + return false; + } + } + } else if (itemCount > actualDeleteCount) { + MOZ_ASSERT(actualDeleteCount <= UINT32_MAX); + uint32_t deleteCount = uint32_t(actualDeleteCount); + + /* Step 17. */ + + // Fast path for when we can simply extend and move the dense elements. + auto extendElements = [len, itemCount, deleteCount](JSContext* cx, + HandleObject obj) { + if (!obj->is<ArrayObject>()) { + return DenseElementResult::Incomplete; + } + if (len > UINT32_MAX) { + return DenseElementResult::Incomplete; + } + + // Ensure there are no getters/setters or other extra indexed properties. + if (ObjectMayHaveExtraIndexedProperties(obj)) { + return DenseElementResult::Incomplete; + } + + // Watch out for arrays with non-writable length or non-extensible arrays. + // In these cases `splice` may have to throw an exception so we let the + // slow path handle it. We also have to ensure we maintain the + // |capacity <= initializedLength| invariant for such objects. See + // NativeObject::shrinkCapacityToInitializedLength. + Handle<ArrayObject*> arr = obj.as<ArrayObject>(); + if (!arr->lengthIsWritable() || !arr->isExtensible()) { + return DenseElementResult::Incomplete; + } + + // Also use the slow path if there might be an active for-in iterator so + // that we don't have to worry about suppressing deleted properties. + if (arr->denseElementsMaybeInIteration()) { + return DenseElementResult::Incomplete; + } + + return arr->ensureDenseElements(cx, uint32_t(len), + itemCount - deleteCount); + }; + + DenseElementResult res = extendElements(cx, obj); + if (res == DenseElementResult::Failure) { + return false; + } + if (res == DenseElementResult::Success) { + MOZ_ASSERT(finalLength <= UINT32_MAX); + MOZ_ASSERT((actualStart + actualDeleteCount) <= len && len <= UINT32_MAX, + "start and deleteCount are uint32 array indices"); + MOZ_ASSERT(actualStart + itemCount <= UINT32_MAX, + "can't overflow because |len - actualDeleteCount + itemCount " + "<= UINT32_MAX| " + "and |actualStart <= len - actualDeleteCount| are both true"); + uint32_t start = uint32_t(actualStart); + uint32_t length = uint32_t(len); + + Handle<ArrayObject*> arr = obj.as<ArrayObject>(); + arr->moveDenseElements(start + itemCount, start + deleteCount, + length - (start + deleteCount)); + + /* Step 20. */ + SetInitializedLength(cx, arr, finalLength); + } else { + MOZ_ASSERT(res == DenseElementResult::Incomplete); + + RootedValue fromValue(cx); + for (uint64_t k = len - actualDeleteCount; k > actualStart; k--) { + if (!CheckForInterrupt(cx)) { + return false; + } + + /* Step 17.b.i. */ + uint64_t from = k + actualDeleteCount - 1; + + /* Step 17.b.ii. */ + uint64_t to = k + itemCount - 1; + + /* Steps 17.b.iii, 17.b.iv.1. */ + bool hole; + if (!HasAndGetElement(cx, obj, from, &hole, &fromValue)) { + return false; + } + + /* Steps 17.b.iv. */ + if (hole) { + /* Step 17.b.v.1. */ + if (!DeletePropertyOrThrow(cx, obj, to)) { + return false; + } + } else { + /* Step 17.b.iv.2. */ + if (!SetArrayElement(cx, obj, to, fromValue)) { + return false; + } + } + } + } + } + + Value* items = args.array() + 2; + + /* Steps 18-19. */ + if (!SetArrayElements(cx, obj, actualStart, itemCount, items)) { + return false; + } + + /* Step 20. */ + if (!SetLengthProperty(cx, obj, finalLength)) { + return false; + } + + /* Step 21. */ + if (returnValueIsUsed) { + args.rval().setObject(*arr); + } + + return true; +} + +/* ES 2016 draft Mar 25, 2016 22.1.3.26. */ +static bool array_splice(JSContext* cx, unsigned argc, Value* vp) { + return array_splice_impl(cx, argc, vp, true); +} + +static bool array_splice_noRetVal(JSContext* cx, unsigned argc, Value* vp) { + return array_splice_impl(cx, argc, vp, false); +} + +static void CopyDenseElementsFillHoles(ArrayObject* arr, NativeObject* nobj, + uint32_t length) { + // Ensure |arr| is an empty array with sufficient capacity. + MOZ_ASSERT(arr->getDenseInitializedLength() == 0); + MOZ_ASSERT(arr->getDenseCapacity() >= length); + MOZ_ASSERT(length > 0); + + uint32_t count = std::min(nobj->getDenseInitializedLength(), length); + + if (count > 0) { + if (nobj->denseElementsArePacked()) { + // Copy all dense elements when no holes are present. + arr->initDenseElements(nobj, 0, count); + } else { + arr->setDenseInitializedLength(count); + + // Handle each element separately to filter out holes. + for (uint32_t i = 0; i < count; i++) { + Value val = nobj->getDenseElement(i); + if (val.isMagic(JS_ELEMENTS_HOLE)) { + val = UndefinedValue(); + } + arr->initDenseElement(i, val); + } + } + } + + // Fill trailing holes with undefined. + if (count < length) { + arr->setDenseInitializedLength(length); + + for (uint32_t i = count; i < length; i++) { + arr->initDenseElement(i, UndefinedValue()); + } + } + + // Ensure |length| elements have been copied and no holes are present. + MOZ_ASSERT(arr->getDenseInitializedLength() == length); + MOZ_ASSERT(arr->denseElementsArePacked()); +} + +// https://github.com/tc39/proposal-change-array-by-copy +// Array.prototype.toSpliced() +static bool array_toSpliced(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "toSpliced"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. Let O be ? ToObject(this value). + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + // Step 2. Let len be ? LengthOfArrayLike(O). + uint64_t len; + if (!GetLengthPropertyInlined(cx, obj, &len)) { + return false; + } + + // Steps 3-6. + // |actualStart| is the index after which elements will be deleted and/or + // new elements will be added + uint64_t actualStart; + if (!GetActualStart(cx, args.get(0), len, &actualStart)) { + return false; + } + MOZ_ASSERT(actualStart <= len); + + // Step 7. Let insertCount be the number of elements in items. + uint32_t insertCount = GetItemCount(args); + + // Steps 8-10. + // actualDeleteCount is the number of elements being deleted + uint64_t actualDeleteCount; + if (!GetActualDeleteCount(cx, args, obj, len, actualStart, insertCount, + &actualDeleteCount)) { + return false; + } + MOZ_ASSERT(actualStart + actualDeleteCount <= len); + + // Step 11. Let newLen be len + insertCount - actualDeleteCount. + uint64_t newLen = len + insertCount - actualDeleteCount; + + // Step 12 handled by GetActualDeleteCount(). + MOZ_ASSERT(newLen < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(actualStart <= newLen, + "if |actualStart + actualDeleteCount <= len| and " + "|newLen = len + insertCount - actualDeleteCount|, then " + "|actualStart <= newLen|"); + + // ArrayCreate, step 1. + if (newLen > UINT32_MAX) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_ARRAY_LENGTH); + return false; + } + + // Step 13. Let A be ? ArrayCreate(𝔽(newLen)). + Rooted<ArrayObject*> arr(cx, + NewDensePartlyAllocatedArray(cx, uint32_t(newLen))); + if (!arr) { + return false; + } + + // Steps 14-19 optimized for dense elements. + if (CanOptimizeForDenseStorage<ArrayAccess::Read>(obj, len)) { + MOZ_ASSERT(len <= UINT32_MAX); + MOZ_ASSERT(actualDeleteCount <= UINT32_MAX, + "if |actualStart + actualDeleteCount <= len| and " + "|len <= UINT32_MAX|, then |actualDeleteCount <= UINT32_MAX|"); + + uint32_t length = uint32_t(len); + uint32_t newLength = uint32_t(newLen); + uint32_t start = uint32_t(actualStart); + uint32_t deleteCount = uint32_t(actualDeleteCount); + + auto nobj = obj.as<NativeObject>(); + + ArrayObject* arr = NewDenseFullyAllocatedArray(cx, newLength); + if (!arr) { + return false; + } + arr->setLength(newLength); + + // Below code doesn't handle the case when the storage has to grow, + // therefore the capacity must fit for at least |newLength| elements. + MOZ_ASSERT(arr->getDenseCapacity() >= newLength); + + if (deleteCount == 0 && insertCount == 0) { + // Copy the array when we don't have to remove or insert any elements. + if (newLength > 0) { + CopyDenseElementsFillHoles(arr, nobj, newLength); + } + } else { + // Copy nobj[0..start] to arr[0..start]. + if (start > 0) { + CopyDenseElementsFillHoles(arr, nobj, start); + } + + // Insert |items| into arr[start..(start + insertCount)]. + if (insertCount > 0) { + auto items = HandleValueArray::subarray(args, 2, insertCount); + + // Prefer |initDenseElements| because it's faster. + if (arr->getDenseInitializedLength() == 0) { + arr->initDenseElements(items.begin(), items.length()); + } else { + arr->ensureDenseInitializedLength(start, items.length()); + arr->copyDenseElements(start, items.begin(), items.length()); + } + } + + uint32_t fromIndex = start + deleteCount; + uint32_t toIndex = start + insertCount; + MOZ_ASSERT((length - fromIndex) == (newLength - toIndex), + "Copies all remaining elements to the end"); + + // Copy nobj[(start + deleteCount)..length] to + // arr[(start + insertCount)..newLength]. + if (fromIndex < length) { + uint32_t end = std::min(length, nobj->getDenseInitializedLength()); + if (fromIndex < end) { + uint32_t count = end - fromIndex; + if (nobj->denseElementsArePacked()) { + // Copy all dense elements when no holes are present. + const Value* src = nobj->getDenseElements() + fromIndex; + arr->ensureDenseInitializedLength(toIndex, count); + arr->copyDenseElements(toIndex, src, count); + fromIndex += count; + toIndex += count; + } else { + arr->setDenseInitializedLength(toIndex + count); + + // Handle each element separately to filter out holes. + for (uint32_t i = 0; i < count; i++) { + Value val = nobj->getDenseElement(fromIndex++); + if (val.isMagic(JS_ELEMENTS_HOLE)) { + val = UndefinedValue(); + } + arr->initDenseElement(toIndex++, val); + } + } + } + + arr->setDenseInitializedLength(newLength); + + // Fill trailing holes with undefined. + while (fromIndex < length) { + arr->initDenseElement(toIndex++, UndefinedValue()); + fromIndex++; + } + } + + MOZ_ASSERT(fromIndex == length); + MOZ_ASSERT(toIndex == newLength); + } + + // Ensure the result array is packed and has the correct length. + MOZ_ASSERT(IsPackedArray(arr)); + MOZ_ASSERT(arr->length() == newLength); + + args.rval().setObject(*arr); + return true; + } + + // Copy everything before start + + // Step 14. Let i be 0. + uint32_t i = 0; + + // Step 15. Let r be actualStart + actualDeleteCount. + uint64_t r = actualStart + actualDeleteCount; + + // Step 16. Repeat while i < actualStart, + RootedValue iValue(cx); + while (i < uint32_t(actualStart)) { + if (!CheckForInterrupt(cx)) { + return false; + } + + // Skip Step 16.a. Let Pi be ! ToString(𝔽(i)). + + // Step 16.b. Let iValue be ? Get(O, Pi). + if (!GetArrayElement(cx, obj, i, &iValue)) { + return false; + } + + // Step 16.c. Perform ! CreateDataPropertyOrThrow(A, Pi, iValue). + if (!DefineArrayElement(cx, arr, i, iValue)) { + return false; + } + + // Step 16.d. Set i to i + 1. + i++; + } + + // Result array now contains all elements before start. + + // Copy new items + if (insertCount > 0) { + HandleValueArray items = HandleValueArray::subarray(args, 2, insertCount); + + // Fast-path to copy all items in one go. + DenseElementResult result = + arr->setOrExtendDenseElements(cx, i, items.begin(), items.length()); + if (result == DenseElementResult::Failure) { + return false; + } + + if (result == DenseElementResult::Success) { + i += items.length(); + } else { + MOZ_ASSERT(result == DenseElementResult::Incomplete); + + // Step 17. For each element E of items, do + for (size_t j = 0; j < items.length(); j++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + // Skip Step 17.a. Let Pi be ! ToString(𝔽(i)). + + // Step 17.b. Perform ! CreateDataPropertyOrThrow(A, Pi, E). + if (!DefineArrayElement(cx, arr, i, items[j])) { + return false; + } + + // Step 17.c. Set i to i + 1. + i++; + } + } + } + + // Copy items after new items + // Step 18. Repeat, while i < newLen, + RootedValue fromValue(cx); + while (i < uint32_t(newLen)) { + if (!CheckForInterrupt(cx)) { + return false; + } + + // Skip Step 18.a. Let Pi be ! ToString(𝔽(i)). + // Skip Step 18.b. Let from be ! ToString(𝔽(r)). + + // Step 18.c. Let fromValue be ? Get(O, from). */ + if (!GetArrayElement(cx, obj, r, &fromValue)) { + return false; + } + + // Step 18.d. Perform ! CreateDataPropertyOrThrow(A, Pi, fromValue). + if (!DefineArrayElement(cx, arr, i, fromValue)) { + return false; + } + + // Step 18.e. Set i to i + 1. + i++; + + // Step 18.f. Set r to r + 1. + r++; + } + + // Step 19. Return A. + args.rval().setObject(*arr); + return true; +} + +// https://github.com/tc39/proposal-change-array-by-copy +// Array.prototype.with() +static bool array_with(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "with"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. Let O be ? ToObject(this value). + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + // Step 2. Let len be ? LengthOfArrayLike(O). + uint64_t len; + if (!GetLengthPropertyInlined(cx, obj, &len)) { + return false; + } + + // Step 3. Let relativeIndex be ? ToIntegerOrInfinity(index). + double relativeIndex; + if (!ToInteger(cx, args.get(0), &relativeIndex)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX); + return false; + } + + // Step 4. If relativeIndex >= 0, let actualIndex be relativeIndex. + double actualIndex = relativeIndex; + if (actualIndex < 0) { + // Step 5. Else, let actualIndex be len + relativeIndex. + actualIndex = double(len) + actualIndex; + } + + // Step 6. If actualIndex >= len or actualIndex < 0, throw a RangeError + // exception. + if (actualIndex < 0 || actualIndex >= double(len)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX); + return false; + } + + // ArrayCreate, step 1. + if (len > UINT32_MAX) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_ARRAY_LENGTH); + return false; + } + uint32_t length = uint32_t(len); + + MOZ_ASSERT(length > 0); + MOZ_ASSERT(0 <= actualIndex && actualIndex < UINT32_MAX); + + // Steps 7-10 optimized for dense elements. + if (CanOptimizeForDenseStorage<ArrayAccess::Read>(obj, length)) { + auto nobj = obj.as<NativeObject>(); + + ArrayObject* arr = NewDenseFullyAllocatedArray(cx, length); + if (!arr) { + return false; + } + arr->setLength(length); + + CopyDenseElementsFillHoles(arr, nobj, length); + + // Replace the value at |actualIndex|. + arr->setDenseElement(uint32_t(actualIndex), args.get(1)); + + // Ensure the result array is packed and has the correct length. + MOZ_ASSERT(IsPackedArray(arr)); + MOZ_ASSERT(arr->length() == length); + + args.rval().setObject(*arr); + return true; + } + + // Step 7. Let A be ? ArrayCreate(𝔽(len)). + RootedObject arr(cx, NewDensePartlyAllocatedArray(cx, length)); + if (!arr) { + return false; + } + + // Steps 8-9. Let k be 0; Repeat, while k < len, + RootedValue fromValue(cx); + for (uint32_t k = 0; k < length; k++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + // Skip Step 9.a. Let Pk be ! ToString(𝔽(k)). + + // Step 9.b. If k is actualIndex, let fromValue be value. + if (k == uint32_t(actualIndex)) { + fromValue = args.get(1); + } else { + // Step 9.c. Else, let fromValue be ? Get(O, 𝔽(k)). + if (!GetArrayElement(cx, obj, k, &fromValue)) { + return false; + } + } + + // Step 9.d. Perform ! CreateDataPropertyOrThrow(A, 𝔽(k), fromValue). + if (!DefineArrayElement(cx, arr, k, fromValue)) { + return false; + } + } + + // Step 10. Return A. + args.rval().setObject(*arr); + return true; +} + +struct SortComparatorIndexes { + bool operator()(uint32_t a, uint32_t b, bool* lessOrEqualp) { + *lessOrEqualp = (a <= b); + return true; + } +}; + +// Returns all indexed properties in the range [begin, end) found on |obj| or +// its proto chain. This function does not handle proxies, objects with +// resolve/lookupProperty hooks or indexed getters, as those can introduce +// new properties. In those cases, *success is set to |false|. +static bool GetIndexedPropertiesInRange(JSContext* cx, HandleObject obj, + uint64_t begin, uint64_t end, + Vector<uint32_t>& indexes, + bool* success) { + *success = false; + + // TODO: Add IdIsIndex with support for large indices. + if (end > UINT32_MAX) { + return true; + } + MOZ_ASSERT(begin <= UINT32_MAX); + + // First, look for proxies or class hooks that can introduce extra + // properties. + JSObject* pobj = obj; + do { + if (!pobj->is<NativeObject>() || pobj->getClass()->getResolve() || + pobj->getOpsLookupProperty()) { + return true; + } + } while ((pobj = pobj->staticPrototype())); + + // Collect indexed property names. + pobj = obj; + do { + // Append dense elements. + NativeObject* nativeObj = &pobj->as<NativeObject>(); + uint32_t initLen = nativeObj->getDenseInitializedLength(); + for (uint32_t i = begin; i < initLen && i < end; i++) { + if (nativeObj->getDenseElement(i).isMagic(JS_ELEMENTS_HOLE)) { + continue; + } + if (!indexes.append(i)) { + return false; + } + } + + // Append typed array elements. + if (nativeObj->is<TypedArrayObject>()) { + size_t len = nativeObj->as<TypedArrayObject>().length(); + for (uint32_t i = begin; i < len && i < end; i++) { + if (!indexes.append(i)) { + return false; + } + } + } + + // Append sparse elements. + if (nativeObj->isIndexed()) { + ShapePropertyIter<NoGC> iter(nativeObj->shape()); + for (; !iter.done(); iter++) { + jsid id = iter->key(); + uint32_t i; + if (!IdIsIndex(id, &i)) { + continue; + } + + if (!(begin <= i && i < end)) { + continue; + } + + // Watch out for getters, they can add new properties. + if (!iter->isDataProperty()) { + return true; + } + + if (!indexes.append(i)) { + return false; + } + } + } + } while ((pobj = pobj->staticPrototype())); + + // Sort the indexes. + Vector<uint32_t> tmp(cx); + size_t n = indexes.length(); + if (!tmp.resize(n)) { + return false; + } + if (!MergeSort(indexes.begin(), n, tmp.begin(), SortComparatorIndexes())) { + return false; + } + + // Remove duplicates. + if (!indexes.empty()) { + uint32_t last = 0; + for (size_t i = 1, len = indexes.length(); i < len; i++) { + uint32_t elem = indexes[i]; + if (indexes[last] != elem) { + last++; + indexes[last] = elem; + } + } + if (!indexes.resize(last + 1)) { + return false; + } + } + + *success = true; + return true; +} + +static bool SliceSparse(JSContext* cx, HandleObject obj, uint64_t begin, + uint64_t end, Handle<ArrayObject*> result) { + MOZ_ASSERT(begin <= end); + + Vector<uint32_t> indexes(cx); + bool success; + if (!GetIndexedPropertiesInRange(cx, obj, begin, end, indexes, &success)) { + return false; + } + + if (!success) { + return CopyArrayElements(cx, obj, begin, end - begin, result); + } + + MOZ_ASSERT(end <= UINT32_MAX, + "indices larger than UINT32_MAX should be rejected by " + "GetIndexedPropertiesInRange"); + + RootedValue value(cx); + for (uint32_t index : indexes) { + MOZ_ASSERT(begin <= index && index < end); + + bool hole; + if (!HasAndGetElement(cx, obj, index, &hole, &value)) { + return false; + } + + if (!hole && + !DefineDataElement(cx, result, index - uint32_t(begin), value)) { + return false; + } + } + + return true; +} + +static JSObject* SliceArguments(JSContext* cx, Handle<ArgumentsObject*> argsobj, + uint32_t begin, uint32_t count) { + MOZ_ASSERT(!argsobj->hasOverriddenLength() && + !argsobj->hasOverriddenElement()); + MOZ_ASSERT(begin + count <= argsobj->initialLength()); + + ArrayObject* result = NewDenseFullyAllocatedArray(cx, count); + if (!result) { + return nullptr; + } + result->setDenseInitializedLength(count); + + for (uint32_t index = 0; index < count; index++) { + const Value& v = argsobj->element(begin + index); + result->initDenseElement(index, v); + } + return result; +} + +template <typename T, typename ArrayLength> +static inline ArrayLength NormalizeSliceTerm(T value, ArrayLength length) { + if (value < 0) { + value += length; + if (value < 0) { + return 0; + } + } else if (double(value) > double(length)) { + return length; + } + return ArrayLength(value); +} + +static bool ArraySliceOrdinary(JSContext* cx, HandleObject obj, uint64_t begin, + uint64_t end, MutableHandleValue rval) { + if (begin > end) { + begin = end; + } + + if ((end - begin) > UINT32_MAX) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_ARRAY_LENGTH); + return false; + } + uint32_t count = uint32_t(end - begin); + + if (CanOptimizeForDenseStorage<ArrayAccess::Read>(obj, end)) { + MOZ_ASSERT(begin <= UINT32_MAX, + "if end <= UINT32_MAX, then begin <= UINT32_MAX"); + JSObject* narr = CopyDenseArrayElements(cx, obj.as<NativeObject>(), + uint32_t(begin), count); + if (!narr) { + return false; + } + + rval.setObject(*narr); + return true; + } + + if (obj->is<ArgumentsObject>()) { + Handle<ArgumentsObject*> argsobj = obj.as<ArgumentsObject>(); + if (!argsobj->hasOverriddenLength() && !argsobj->hasOverriddenElement()) { + MOZ_ASSERT(begin <= UINT32_MAX, "begin is limited by |argsobj|'s length"); + JSObject* narr = SliceArguments(cx, argsobj, uint32_t(begin), count); + if (!narr) { + return false; + } + + rval.setObject(*narr); + return true; + } + } + + Rooted<ArrayObject*> narr(cx, NewDensePartlyAllocatedArray(cx, count)); + if (!narr) { + return false; + } + + if (end <= UINT32_MAX) { + if (js::GetElementsOp op = obj->getOpsGetElements()) { + ElementAdder adder(cx, narr, count, + ElementAdder::CheckHasElemPreserveHoles); + if (!op(cx, obj, uint32_t(begin), uint32_t(end), &adder)) { + return false; + } + + rval.setObject(*narr); + return true; + } + } + + if (obj->is<NativeObject>() && obj->as<NativeObject>().isIndexed() && + count > 1000) { + if (!SliceSparse(cx, obj, begin, end, narr)) { + return false; + } + } else { + if (!CopyArrayElements(cx, obj, begin, count, narr)) { + return false; + } + } + + rval.setObject(*narr); + return true; +} + +/* ES 2016 draft Mar 25, 2016 22.1.3.23. */ +static bool array_slice(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "slice"); + CallArgs args = CallArgsFromVp(argc, vp); + + /* Step 1. */ + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + /* Step 2. */ + uint64_t length; + if (!GetLengthPropertyInlined(cx, obj, &length)) { + return false; + } + + uint64_t k = 0; + uint64_t final = length; + if (args.length() > 0) { + double d; + /* Step 3. */ + if (!ToInteger(cx, args[0], &d)) { + return false; + } + + /* Step 4. */ + k = NormalizeSliceTerm(d, length); + + if (args.hasDefined(1)) { + /* Step 5. */ + if (!ToInteger(cx, args[1], &d)) { + return false; + } + + /* Step 6. */ + final = NormalizeSliceTerm(d, length); + } + } + + if (IsArraySpecies(cx, obj)) { + /* Steps 7-12: Optimized for ordinary array. */ + return ArraySliceOrdinary(cx, obj, k, final, args.rval()); + } + + /* Step 7. */ + uint64_t count = final > k ? final - k : 0; + + /* Step 8. */ + RootedObject arr(cx); + if (!ArraySpeciesCreate(cx, obj, count, &arr)) { + return false; + } + + /* Step 9. */ + uint64_t n = 0; + + /* Step 10. */ + RootedValue kValue(cx); + while (k < final) { + if (!CheckForInterrupt(cx)) { + return false; + } + + /* Steps 10.a-b, and 10.c.i. */ + bool kNotPresent; + if (!HasAndGetElement(cx, obj, k, &kNotPresent, &kValue)) { + return false; + } + + /* Step 10.c. */ + if (!kNotPresent) { + /* Steps 10.c.ii. */ + if (!DefineArrayElement(cx, arr, n, kValue)) { + return false; + } + } + /* Step 10.d. */ + k++; + + /* Step 10.e. */ + n++; + } + + /* Step 11. */ + if (!SetLengthProperty(cx, arr, n)) { + return false; + } + + /* Step 12. */ + args.rval().setObject(*arr); + return true; +} + +static bool ArraySliceDenseKernel(JSContext* cx, ArrayObject* arr, + int32_t beginArg, int32_t endArg, + ArrayObject* result) { + uint32_t length = arr->length(); + + uint32_t begin = NormalizeSliceTerm(beginArg, length); + uint32_t end = NormalizeSliceTerm(endArg, length); + + if (begin > end) { + begin = end; + } + + uint32_t count = end - begin; + size_t initlen = arr->getDenseInitializedLength(); + if (initlen > begin) { + uint32_t newlength = std::min<uint32_t>(initlen - begin, count); + if (newlength > 0) { + if (!result->ensureElements(cx, newlength)) { + return false; + } + result->initDenseElements(arr, begin, newlength); + } + } + + MOZ_ASSERT(count >= result->length()); + result->setLength(count); + + return true; +} + +JSObject* js::ArraySliceDense(JSContext* cx, HandleObject obj, int32_t begin, + int32_t end, HandleObject result) { + MOZ_ASSERT(IsPackedArray(obj)); + + if (result && IsArraySpecies(cx, obj)) { + if (!ArraySliceDenseKernel(cx, &obj->as<ArrayObject>(), begin, end, + &result->as<ArrayObject>())) { + return nullptr; + } + return result; + } + + // Slower path if the JIT wasn't able to allocate an object inline. + JS::RootedValueArray<4> argv(cx); + argv[0].setUndefined(); + argv[1].setObject(*obj); + argv[2].setInt32(begin); + argv[3].setInt32(end); + if (!array_slice(cx, 2, argv.begin())) { + return nullptr; + } + return &argv[0].toObject(); +} + +JSObject* js::ArgumentsSliceDense(JSContext* cx, HandleObject obj, + int32_t begin, int32_t end, + HandleObject result) { + MOZ_ASSERT(obj->is<ArgumentsObject>()); + MOZ_ASSERT(IsArraySpecies(cx, obj)); + + Handle<ArgumentsObject*> argsobj = obj.as<ArgumentsObject>(); + MOZ_ASSERT(!argsobj->hasOverriddenLength()); + MOZ_ASSERT(!argsobj->hasOverriddenElement()); + + uint32_t length = argsobj->initialLength(); + uint32_t actualBegin = NormalizeSliceTerm(begin, length); + uint32_t actualEnd = NormalizeSliceTerm(end, length); + + if (actualBegin > actualEnd) { + actualBegin = actualEnd; + } + uint32_t count = actualEnd - actualBegin; + + if (result) { + Handle<ArrayObject*> resArray = result.as<ArrayObject>(); + MOZ_ASSERT(resArray->getDenseInitializedLength() == 0); + MOZ_ASSERT(resArray->length() == 0); + + if (count > 0) { + if (!resArray->ensureElements(cx, count)) { + return nullptr; + } + resArray->setDenseInitializedLength(count); + resArray->setLength(count); + + for (uint32_t index = 0; index < count; index++) { + const Value& v = argsobj->element(actualBegin + index); + resArray->initDenseElement(index, v); + } + } + + return resArray; + } + + // Slower path if the JIT wasn't able to allocate an object inline. + return SliceArguments(cx, argsobj, actualBegin, count); +} + +static bool array_isArray(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array", "isArray"); + CallArgs args = CallArgsFromVp(argc, vp); + + bool isArray = false; + if (args.get(0).isObject()) { + RootedObject obj(cx, &args[0].toObject()); + if (!IsArray(cx, obj, &isArray)) { + return false; + } + } + args.rval().setBoolean(isArray); + return true; +} + +static bool ArrayFromCallArgs(JSContext* cx, CallArgs& args, + HandleObject proto = nullptr) { + ArrayObject* obj = + NewDenseCopiedArrayWithProto(cx, args.length(), args.array(), proto); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +static bool array_of(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array", "of"); + CallArgs args = CallArgsFromVp(argc, vp); + + bool isArrayConstructor = + IsArrayConstructor(args.thisv()) && + args.thisv().toObject().nonCCWRealm() == cx->realm(); + + if (isArrayConstructor || !IsConstructor(args.thisv())) { + // isArrayConstructor will usually be true in practice. This is the most + // common path. + return ArrayFromCallArgs(cx, args); + } + + // Step 4. + RootedObject obj(cx); + { + FixedConstructArgs<1> cargs(cx); + + cargs[0].setNumber(args.length()); + + if (!Construct(cx, args.thisv(), cargs, args.thisv(), &obj)) { + return false; + } + } + + // Step 8. + for (unsigned k = 0; k < args.length(); k++) { + if (!DefineDataElement(cx, obj, k, args[k])) { + return false; + } + } + + // Steps 9-10. + if (!SetLengthProperty(cx, obj, args.length())) { + return false; + } + + // Step 11. + args.rval().setObject(*obj); + return true; +} + +static const JSJitInfo array_splice_info = { + {(JSJitGetterOp)array_splice_noRetVal}, + {0}, /* unused */ + {0}, /* unused */ + JSJitInfo::IgnoresReturnValueNative, + JSJitInfo::AliasEverything, + JSVAL_TYPE_UNDEFINED, +}; + +enum class SearchKind { + // Specializes SearchElementDense for Array.prototype.indexOf/lastIndexOf. + // This means hole values are ignored and StrictlyEqual semantics are used. + IndexOf, + // Specializes SearchElementDense for Array.prototype.includes. + // This means hole values are treated as |undefined| and SameValueZero + // semantics are used. + Includes, +}; + +template <SearchKind Kind, typename Iter> +static bool SearchElementDense(JSContext* cx, HandleValue val, Iter iterator, + MutableHandleValue rval) { + // We assume here and in the iterator lambdas that nothing can trigger GC or + // move dense elements. + AutoCheckCannotGC nogc; + + // Fast path for string values. + if (val.isString()) { + JSLinearString* str = val.toString()->ensureLinear(cx); + if (!str) { + return false; + } + const uint32_t strLen = str->length(); + auto cmp = [str, strLen](JSContext* cx, const Value& element, bool* equal) { + if (!element.isString() || element.toString()->length() != strLen) { + *equal = false; + return true; + } + JSLinearString* s = element.toString()->ensureLinear(cx); + if (!s) { + return false; + } + *equal = EqualStrings(str, s); + return true; + }; + return iterator(cx, cmp, rval); + } + + // Fast path for numbers. + if (val.isNumber()) { + double dval = val.toNumber(); + // For |includes|, two NaN values are considered equal, so we use a + // different implementation for NaN. + if (Kind == SearchKind::Includes && std::isnan(dval)) { + auto cmp = [](JSContext*, const Value& element, bool* equal) { + *equal = (element.isDouble() && std::isnan(element.toDouble())); + return true; + }; + return iterator(cx, cmp, rval); + } + auto cmp = [dval](JSContext*, const Value& element, bool* equal) { + *equal = (element.isNumber() && element.toNumber() == dval); + return true; + }; + return iterator(cx, cmp, rval); + } + + // Fast path for values where we can use a simple bitwise comparison. + if (CanUseBitwiseCompareForStrictlyEqual(val)) { + // For |includes| we need to treat hole values as |undefined| so we use a + // different path if searching for |undefined|. + if (Kind == SearchKind::Includes && val.isUndefined()) { + auto cmp = [](JSContext*, const Value& element, bool* equal) { + *equal = (element.isUndefined() || element.isMagic(JS_ELEMENTS_HOLE)); + return true; + }; + return iterator(cx, cmp, rval); + } + uint64_t bits = val.asRawBits(); + auto cmp = [bits](JSContext*, const Value& element, bool* equal) { + *equal = (bits == element.asRawBits()); + return true; + }; + return iterator(cx, cmp, rval); + } + + MOZ_ASSERT(val.isBigInt() || + IF_RECORD_TUPLE(val.isExtendedPrimitive(), false)); + + // Generic implementation for the remaining types. + RootedValue elementRoot(cx); + auto cmp = [val, &elementRoot](JSContext* cx, const Value& element, + bool* equal) { + if (MOZ_UNLIKELY(element.isMagic(JS_ELEMENTS_HOLE))) { + // |includes| treats holes as |undefined|, but |undefined| is already + // handled above. For |indexOf| we have to ignore holes. + *equal = false; + return true; + } + // Note: |includes| uses SameValueZero, but that checks for NaN and then + // calls StrictlyEqual. Since we already handled NaN above, we can call + // StrictlyEqual directly. + MOZ_ASSERT(!val.isNumber()); + elementRoot = element; + return StrictlyEqual(cx, val, elementRoot, equal); + }; + return iterator(cx, cmp, rval); +} + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 22.1.3.14 Array.prototype.indexOf ( searchElement [ , fromIndex ] ) +bool js::array_indexOf(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "indexOf"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + // Step 2. + uint64_t len; + if (!GetLengthPropertyInlined(cx, obj, &len)) { + return false; + } + + // Step 3. + if (len == 0) { + args.rval().setInt32(-1); + return true; + } + + // Steps 4-8. + uint64_t k = 0; + if (args.length() > 1) { + double n; + if (!ToInteger(cx, args[1], &n)) { + return false; + } + + // Step 6. + if (n >= double(len)) { + args.rval().setInt32(-1); + return true; + } + + // Steps 7-8. + if (n >= 0) { + k = uint64_t(n); + } else { + double d = double(len) + n; + if (d >= 0) { + k = uint64_t(d); + } + } + } + + MOZ_ASSERT(k < len); + + HandleValue searchElement = args.get(0); + + // Steps 9 and 10 optimized for dense elements. + if (CanOptimizeForDenseStorage<ArrayAccess::Read>(obj, len)) { + MOZ_ASSERT(len <= UINT32_MAX); + + NativeObject* nobj = &obj->as<NativeObject>(); + uint32_t start = uint32_t(k); + uint32_t length = + std::min(nobj->getDenseInitializedLength(), uint32_t(len)); + const Value* elements = nobj->getDenseElements(); + + if (CanUseBitwiseCompareForStrictlyEqual(searchElement) && length > start) { + const uint64_t* elementsAsBits = + reinterpret_cast<const uint64_t*>(elements); + const uint64_t* res = SIMD::memchr64( + elementsAsBits + start, searchElement.asRawBits(), length - start); + if (res) { + args.rval().setInt32(static_cast<int32_t>(res - elementsAsBits)); + } else { + args.rval().setInt32(-1); + } + return true; + } + + auto iterator = [elements, start, length](JSContext* cx, auto cmp, + MutableHandleValue rval) { + static_assert(NativeObject::MAX_DENSE_ELEMENTS_COUNT <= INT32_MAX, + "code assumes dense index fits in Int32Value"); + for (uint32_t i = start; i < length; i++) { + bool equal; + if (MOZ_UNLIKELY(!cmp(cx, elements[i], &equal))) { + return false; + } + if (equal) { + rval.setInt32(int32_t(i)); + return true; + } + } + rval.setInt32(-1); + return true; + }; + return SearchElementDense<SearchKind::IndexOf>(cx, searchElement, iterator, + args.rval()); + } + + // Step 9. + RootedValue v(cx); + for (; k < len; k++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + bool hole; + if (!HasAndGetElement(cx, obj, k, &hole, &v)) { + return false; + } + if (hole) { + continue; + } + + bool equal; + if (!StrictlyEqual(cx, v, searchElement, &equal)) { + return false; + } + if (equal) { + args.rval().setNumber(k); + return true; + } + } + + // Step 10. + args.rval().setInt32(-1); + return true; +} + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 22.1.3.17 Array.prototype.lastIndexOf ( searchElement [ , fromIndex ] ) +bool js::array_lastIndexOf(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "lastIndexOf"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + // Step 2. + uint64_t len; + if (!GetLengthPropertyInlined(cx, obj, &len)) { + return false; + } + + // Step 3. + if (len == 0) { + args.rval().setInt32(-1); + return true; + } + + // Steps 4-6. + uint64_t k = len - 1; + if (args.length() > 1) { + double n; + if (!ToInteger(cx, args[1], &n)) { + return false; + } + + // Steps 5-6. + if (n < 0) { + double d = double(len) + n; + if (d < 0) { + args.rval().setInt32(-1); + return true; + } + k = uint64_t(d); + } else if (n < double(k)) { + k = uint64_t(n); + } + } + + MOZ_ASSERT(k < len); + + HandleValue searchElement = args.get(0); + + // Steps 7 and 8 optimized for dense elements. + if (CanOptimizeForDenseStorage<ArrayAccess::Read>(obj, k + 1)) { + MOZ_ASSERT(k <= UINT32_MAX); + + NativeObject* nobj = &obj->as<NativeObject>(); + uint32_t initLen = nobj->getDenseInitializedLength(); + if (initLen == 0) { + args.rval().setInt32(-1); + return true; + } + + uint32_t end = std::min(uint32_t(k), initLen - 1); + const Value* elements = nobj->getDenseElements(); + + auto iterator = [elements, end](JSContext* cx, auto cmp, + MutableHandleValue rval) { + static_assert(NativeObject::MAX_DENSE_ELEMENTS_COUNT <= INT32_MAX, + "code assumes dense index fits in int32_t"); + for (int32_t i = int32_t(end); i >= 0; i--) { + bool equal; + if (MOZ_UNLIKELY(!cmp(cx, elements[i], &equal))) { + return false; + } + if (equal) { + rval.setInt32(int32_t(i)); + return true; + } + } + rval.setInt32(-1); + return true; + }; + return SearchElementDense<SearchKind::IndexOf>(cx, searchElement, iterator, + args.rval()); + } + + // Step 7. + RootedValue v(cx); + for (int64_t i = int64_t(k); i >= 0; i--) { + if (!CheckForInterrupt(cx)) { + return false; + } + + bool hole; + if (!HasAndGetElement(cx, obj, uint64_t(i), &hole, &v)) { + return false; + } + if (hole) { + continue; + } + + bool equal; + if (!StrictlyEqual(cx, v, searchElement, &equal)) { + return false; + } + if (equal) { + args.rval().setNumber(uint64_t(i)); + return true; + } + } + + // Step 8. + args.rval().setInt32(-1); + return true; +} + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 22.1.3.13 Array.prototype.includes ( searchElement [ , fromIndex ] ) +bool js::array_includes(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "includes"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + // Step 2. + uint64_t len; + if (!GetLengthPropertyInlined(cx, obj, &len)) { + return false; + } + + // Step 3. + if (len == 0) { + args.rval().setBoolean(false); + return true; + } + + // Steps 4-7. + uint64_t k = 0; + if (args.length() > 1) { + double n; + if (!ToInteger(cx, args[1], &n)) { + return false; + } + + if (n >= double(len)) { + args.rval().setBoolean(false); + return true; + } + + // Steps 6-7. + if (n >= 0) { + k = uint64_t(n); + } else { + double d = double(len) + n; + if (d >= 0) { + k = uint64_t(d); + } + } + } + + MOZ_ASSERT(k < len); + + HandleValue searchElement = args.get(0); + + // Steps 8 and 9 optimized for dense elements. + if (CanOptimizeForDenseStorage<ArrayAccess::Read>(obj, len)) { + MOZ_ASSERT(len <= UINT32_MAX); + + NativeObject* nobj = &obj->as<NativeObject>(); + uint32_t start = uint32_t(k); + uint32_t length = + std::min(nobj->getDenseInitializedLength(), uint32_t(len)); + const Value* elements = nobj->getDenseElements(); + + // Trailing holes are treated as |undefined|. + if (uint32_t(len) > length && searchElement.isUndefined()) { + // |undefined| is strictly equal only to |undefined|. + args.rval().setBoolean(true); + return true; + } + + // For |includes| we need to treat hole values as |undefined| so we use a + // different path if searching for |undefined|. + if (CanUseBitwiseCompareForStrictlyEqual(searchElement) && + !searchElement.isUndefined() && length > start) { + if (SIMD::memchr64(reinterpret_cast<const uint64_t*>(elements) + start, + searchElement.asRawBits(), length - start)) { + args.rval().setBoolean(true); + } else { + args.rval().setBoolean(false); + } + return true; + } + + auto iterator = [elements, start, length](JSContext* cx, auto cmp, + MutableHandleValue rval) { + for (uint32_t i = start; i < length; i++) { + bool equal; + if (MOZ_UNLIKELY(!cmp(cx, elements[i], &equal))) { + return false; + } + if (equal) { + rval.setBoolean(true); + return true; + } + } + rval.setBoolean(false); + return true; + }; + return SearchElementDense<SearchKind::Includes>(cx, searchElement, iterator, + args.rval()); + } + + // Step 8. + RootedValue v(cx); + for (; k < len; k++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + if (!GetArrayElement(cx, obj, k, &v)) { + return false; + } + + bool equal; + if (!SameValueZero(cx, v, searchElement, &equal)) { + return false; + } + if (equal) { + args.rval().setBoolean(true); + return true; + } + } + + // Step 9. + args.rval().setBoolean(false); + return true; +} + +// ES2024 draft 23.1.3.2.1 IsConcatSpreadable +static bool IsConcatSpreadable(JSContext* cx, HandleValue v, bool* spreadable) { + // Step 1. + if (!v.isObject()) { + *spreadable = false; + return true; + } + + // Step 2. + JS::Symbol* sym = cx->wellKnownSymbols().isConcatSpreadable; + JSObject* holder; + if (MOZ_UNLIKELY( + MaybeHasInterestingSymbolProperty(cx, &v.toObject(), sym, &holder))) { + RootedValue res(cx); + RootedObject obj(cx, holder); + Rooted<PropertyKey> key(cx, PropertyKey::Symbol(sym)); + if (!GetProperty(cx, obj, v, key, &res)) { + return false; + } + // Step 3. + if (!res.isUndefined()) { + *spreadable = ToBoolean(res); + return true; + } + } + + // Step 4. + if (MOZ_LIKELY(v.toObject().is<ArrayObject>())) { + *spreadable = true; + return true; + } + RootedObject obj(cx, &v.toObject()); + bool isArray; + if (!JS::IsArray(cx, obj, &isArray)) { + return false; + } + *spreadable = isArray; + return true; +} + +// Returns true if the object may have an @@isConcatSpreadable property. +static bool MaybeHasIsConcatSpreadable(JSContext* cx, JSObject* obj) { + JS::Symbol* sym = cx->wellKnownSymbols().isConcatSpreadable; + JSObject* holder; + return MaybeHasInterestingSymbolProperty(cx, obj, sym, &holder); +} + +static bool TryOptimizePackedArrayConcat(JSContext* cx, CallArgs& args, + Handle<JSObject*> obj, + bool* optimized) { + // Fast path for the following cases: + // + // (1) packedArray.concat(): copy the array's elements. + // (2) packedArray.concat(packedArray): concatenate two packed arrays. + // (3) packedArray.concat(value): copy and append a single non-array value. + // + // These cases account for almost all calls to Array.prototype.concat in + // Speedometer 3. + + *optimized = false; + + if (args.length() > 1) { + return true; + } + + // The `this` object must be a packed array without @@isConcatSpreadable. + // @@isConcatSpreadable is uncommon and requires a property lookup and more + // complicated code, so we let the slow path handle it. + if (!IsPackedArray(obj)) { + return true; + } + if (MaybeHasIsConcatSpreadable(cx, obj)) { + return true; + } + + Handle<ArrayObject*> thisArr = obj.as<ArrayObject>(); + uint32_t thisLen = thisArr->length(); + + if (args.length() == 0) { + // Case (1). Copy the packed array. + ArrayObject* arr = NewDenseFullyAllocatedArray(cx, thisLen); + if (!arr) { + return false; + } + arr->initDenseElements(thisArr->getDenseElements(), thisLen); + args.rval().setObject(*arr); + *optimized = true; + return true; + } + + MOZ_ASSERT(args.length() == 1); + + // If the argument is an object, it must not have an @@isConcatSpreadable + // property. + if (args[0].isObject() && + MaybeHasIsConcatSpreadable(cx, &args[0].toObject())) { + return true; + } + + MOZ_ASSERT_IF(args[0].isObject(), args[0].toObject().is<NativeObject>()); + + // Case (3). Copy and append a single value if the argument is not an array. + if (!args[0].isObject() || !args[0].toObject().is<ArrayObject>()) { + ArrayObject* arr = NewDenseFullyAllocatedArray(cx, thisLen + 1); + if (!arr) { + return false; + } + arr->initDenseElements(thisArr->getDenseElements(), thisLen); + + arr->ensureDenseInitializedLength(thisLen, 1); + arr->initDenseElement(thisLen, args[0]); + + args.rval().setObject(*arr); + *optimized = true; + return true; + } + + // Case (2). Concatenate two packed arrays. + if (!IsPackedArray(&args[0].toObject())) { + return true; + } + + uint32_t argLen = args[0].toObject().as<ArrayObject>().length(); + + // Compute the array length. This can't overflow because both arrays are + // packed. + static_assert(NativeObject::MAX_DENSE_ELEMENTS_COUNT < INT32_MAX); + MOZ_ASSERT(thisLen <= NativeObject::MAX_DENSE_ELEMENTS_COUNT); + MOZ_ASSERT(argLen <= NativeObject::MAX_DENSE_ELEMENTS_COUNT); + uint32_t totalLen = thisLen + argLen; + + ArrayObject* arr = NewDenseFullyAllocatedArray(cx, totalLen); + if (!arr) { + return false; + } + arr->initDenseElements(thisArr->getDenseElements(), thisLen); + + ArrayObject* argArr = &args[0].toObject().as<ArrayObject>(); + arr->ensureDenseInitializedLength(thisLen, argLen); + arr->initDenseElementRange(thisLen, argArr, argLen); + + args.rval().setObject(*arr); + *optimized = true; + return true; +} + +static bool array_concat(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Array.prototype", "concat"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + bool isArraySpecies = IsArraySpecies(cx, obj); + + // Fast path for the most common cases. + if (isArraySpecies) { + bool optimized; + if (!TryOptimizePackedArrayConcat(cx, args, obj, &optimized)) { + return false; + } + if (optimized) { + return true; + } + } + + // Step 2. + RootedObject arr(cx); + if (isArraySpecies) { + arr = NewDenseEmptyArray(cx); + if (!arr) { + return false; + } + } else { + if (!ArraySpeciesCreate(cx, obj, 0, &arr)) { + return false; + } + } + + // Step 3. + uint64_t n = 0; + + // Step 4 (handled implicitly with nextArg and CallArgs). + uint32_t nextArg = 0; + + // Step 5. + RootedValue v(cx, ObjectValue(*obj)); + while (true) { + // Step 5.a. + bool spreadable; + if (!IsConcatSpreadable(cx, v, &spreadable)) { + return false; + } + // Step 5.b. + if (spreadable) { + // Step 5.b.i. + obj = &v.toObject(); + uint64_t len; + if (!GetLengthPropertyInlined(cx, obj, &len)) { + return false; + } + + // Step 5.b.ii. + if (n + len > uint64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT) - 1) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TOO_LONG_ARRAY); + return false; + } + + // Step 5.b.iii. + uint64_t k = 0; + + // Step 5.b.iv. + + // Try a fast path for copying dense elements directly. + bool optimized = false; + if (len > 0 && isArraySpecies && + CanOptimizeForDenseStorage<ArrayAccess::Read>(obj, len) && + n + len <= NativeObject::MAX_DENSE_ELEMENTS_COUNT) { + NativeObject* nobj = &obj->as<NativeObject>(); + ArrayObject* resArr = &arr->as<ArrayObject>(); + uint32_t count = + std::min(uint32_t(len), nobj->getDenseInitializedLength()); + + DenseElementResult res = resArr->ensureDenseElements(cx, n, count); + if (res == DenseElementResult::Failure) { + return false; + } + if (res == DenseElementResult::Success) { + resArr->initDenseElementRange(n, nobj, count); + n += len; + optimized = true; + } else { + MOZ_ASSERT(res == DenseElementResult::Incomplete); + } + } + + if (!optimized) { + // Step 5.b.iv. + while (k < len) { + if (!CheckForInterrupt(cx)) { + return false; + } + + // Step 5.b.iv.2. + bool hole; + if (!HasAndGetElement(cx, obj, k, &hole, &v)) { + return false; + } + if (!hole) { + // Step 5.b.iv.3. + if (!DefineArrayElement(cx, arr, n, v)) { + return false; + } + } + + // Step 5.b.iv.4. + n++; + + // Step 5.b.iv.5. + k++; + } + } + } else { + // Step 5.c.ii. + if (n >= uint64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT) - 1) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TOO_LONG_ARRAY); + return false; + } + + // Step 5.c.iii. + if (!DefineArrayElement(cx, arr, n, v)) { + return false; + } + + // Step 5.c.iv. + n++; + } + + // Move on to the next argument. + if (nextArg == args.length()) { + break; + } + v = args[nextArg]; + nextArg++; + } + + // Step 6. + if (!SetLengthProperty(cx, arr, n)) { + return false; + } + + // Step 7. + args.rval().setObject(*arr); + return true; +} + +static const JSFunctionSpec array_methods[] = { + JS_FN(js_toSource_str, array_toSource, 0, 0), + JS_SELF_HOSTED_FN(js_toString_str, "ArrayToString", 0, 0), + JS_FN(js_toLocaleString_str, array_toLocaleString, 0, 0), + + /* Perl-ish methods. */ + JS_INLINABLE_FN("join", array_join, 1, 0, ArrayJoin), + JS_FN("reverse", array_reverse, 0, 0), + JS_SELF_HOSTED_FN("sort", "ArraySort", 1, 0), + JS_INLINABLE_FN("push", array_push, 1, 0, ArrayPush), + JS_INLINABLE_FN("pop", array_pop, 0, 0, ArrayPop), + JS_INLINABLE_FN("shift", array_shift, 0, 0, ArrayShift), + JS_FN("unshift", array_unshift, 1, 0), + JS_FNINFO("splice", array_splice, &array_splice_info, 2, 0), + + /* Pythonic sequence methods. */ + JS_FN("concat", array_concat, 1, 0), + JS_INLINABLE_FN("slice", array_slice, 2, 0, ArraySlice), + + JS_FN("lastIndexOf", array_lastIndexOf, 1, 0), + JS_FN("indexOf", array_indexOf, 1, 0), + JS_SELF_HOSTED_FN("forEach", "ArrayForEach", 1, 0), + JS_SELF_HOSTED_FN("map", "ArrayMap", 1, 0), + JS_SELF_HOSTED_FN("filter", "ArrayFilter", 1, 0), +#ifdef NIGHTLY_BUILD + JS_SELF_HOSTED_FN("group", "ArrayGroup", 1, 0), + JS_SELF_HOSTED_FN("groupToMap", "ArrayGroupToMap", 1, 0), +#endif + JS_SELF_HOSTED_FN("reduce", "ArrayReduce", 1, 0), + JS_SELF_HOSTED_FN("reduceRight", "ArrayReduceRight", 1, 0), + JS_SELF_HOSTED_FN("some", "ArraySome", 1, 0), + JS_SELF_HOSTED_FN("every", "ArrayEvery", 1, 0), + + /* ES6 additions */ + JS_SELF_HOSTED_FN("find", "ArrayFind", 1, 0), + JS_SELF_HOSTED_FN("findIndex", "ArrayFindIndex", 1, 0), + JS_SELF_HOSTED_FN("copyWithin", "ArrayCopyWithin", 3, 0), + + JS_SELF_HOSTED_FN("fill", "ArrayFill", 3, 0), + + JS_SELF_HOSTED_SYM_FN(iterator, "$ArrayValues", 0, 0), + JS_SELF_HOSTED_FN("entries", "ArrayEntries", 0, 0), + JS_SELF_HOSTED_FN("keys", "ArrayKeys", 0, 0), + JS_SELF_HOSTED_FN("values", "$ArrayValues", 0, 0), + + /* ES7 additions */ + JS_FN("includes", array_includes, 1, 0), + + /* ES2020 */ + JS_SELF_HOSTED_FN("flatMap", "ArrayFlatMap", 1, 0), + JS_SELF_HOSTED_FN("flat", "ArrayFlat", 0, 0), + + /* Proposal */ + JS_SELF_HOSTED_FN("at", "ArrayAt", 1, 0), + JS_SELF_HOSTED_FN("findLast", "ArrayFindLast", 1, 0), + JS_SELF_HOSTED_FN("findLastIndex", "ArrayFindLastIndex", 1, 0), + + JS_SELF_HOSTED_FN("toReversed", "ArrayToReversed", 0, 0), + JS_SELF_HOSTED_FN("toSorted", "ArrayToSorted", 1, 0), + JS_FN("toSpliced", array_toSpliced, 2, 0), JS_FN("with", array_with, 2, 0), + + JS_FS_END}; + +static const JSFunctionSpec array_static_methods[] = { + JS_INLINABLE_FN("isArray", array_isArray, 1, 0, ArrayIsArray), + JS_SELF_HOSTED_FN("from", "ArrayFrom", 3, 0), + JS_SELF_HOSTED_FN("fromAsync", "ArrayFromAsync", 3, 0), + JS_FN("of", array_of, 0, 0), + + JS_FS_END}; + +const JSPropertySpec array_static_props[] = { + JS_SELF_HOSTED_SYM_GET(species, "$ArraySpecies", 0), JS_PS_END}; + +static inline bool ArrayConstructorImpl(JSContext* cx, CallArgs& args, + bool isConstructor) { + RootedObject proto(cx); + if (isConstructor) { + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Array, &proto)) { + return false; + } + } + + if (args.length() != 1 || !args[0].isNumber()) { + return ArrayFromCallArgs(cx, args, proto); + } + + uint32_t length; + if (args[0].isInt32()) { + int32_t i = args[0].toInt32(); + if (i < 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_ARRAY_LENGTH); + return false; + } + length = uint32_t(i); + } else { + double d = args[0].toDouble(); + length = ToUint32(d); + if (d != double(length)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_ARRAY_LENGTH); + return false; + } + } + + ArrayObject* obj = NewDensePartlyAllocatedArrayWithProto(cx, length, proto); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/* ES5 15.4.2 */ +bool js::ArrayConstructor(JSContext* cx, unsigned argc, Value* vp) { + AutoJSConstructorProfilerEntry pseudoFrame(cx, "Array"); + CallArgs args = CallArgsFromVp(argc, vp); + return ArrayConstructorImpl(cx, args, /* isConstructor = */ true); +} + +bool js::array_construct(JSContext* cx, unsigned argc, Value* vp) { + AutoJSConstructorProfilerEntry pseudoFrame(cx, "Array"); + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(!args.isConstructing()); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isNumber()); + return ArrayConstructorImpl(cx, args, /* isConstructor = */ false); +} + +ArrayObject* js::ArrayConstructorOneArg(JSContext* cx, + Handle<ArrayObject*> templateObject, + int32_t lengthInt) { + // JIT code can call this with a template object from a different realm when + // calling another realm's Array constructor. + Maybe<AutoRealm> ar; + if (cx->realm() != templateObject->realm()) { + MOZ_ASSERT(cx->compartment() == templateObject->compartment()); + ar.emplace(cx, templateObject); + } + + if (lengthInt < 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_ARRAY_LENGTH); + return nullptr; + } + + uint32_t length = uint32_t(lengthInt); + ArrayObject* res = NewDensePartlyAllocatedArray(cx, length); + MOZ_ASSERT_IF(res, res->realm() == templateObject->realm()); + return res; +} + +/* + * Array allocation functions. + */ + +static inline bool EnsureNewArrayElements(JSContext* cx, ArrayObject* obj, + uint32_t length) { + /* + * If ensureElements creates dynamically allocated slots, then having + * fixedSlots is a waste. + */ + DebugOnly<uint32_t> cap = obj->getDenseCapacity(); + + if (!obj->ensureElements(cx, length)) { + return false; + } + + MOZ_ASSERT_IF(cap, !obj->hasDynamicElements()); + + return true; +} + +template <uint32_t maxLength> +static MOZ_ALWAYS_INLINE ArrayObject* NewArrayWithShape( + JSContext* cx, Handle<SharedShape*> shape, uint32_t length, + NewObjectKind newKind, gc::AllocSite* site = nullptr) { + // The shape must already have the |length| property defined on it. + MOZ_ASSERT(shape->propMapLength() == 1); + MOZ_ASSERT(shape->lastProperty().key() == NameToId(cx->names().length)); + + gc::AllocKind allocKind = GuessArrayGCKind(length); + MOZ_ASSERT(CanChangeToBackgroundAllocKind(allocKind, &ArrayObject::class_)); + allocKind = ForegroundToBackgroundAllocKind(allocKind); + + MOZ_ASSERT(shape->slotSpan() == 0); + constexpr uint32_t slotSpan = 0; + + AutoSetNewObjectMetadata metadata(cx); + ArrayObject* arr = ArrayObject::create( + cx, allocKind, GetInitialHeap(newKind, &ArrayObject::class_, site), shape, + length, slotSpan, metadata); + if (!arr) { + return nullptr; + } + + if (maxLength > 0 && + !EnsureNewArrayElements(cx, arr, std::min(maxLength, length))) { + return nullptr; + } + + probes::CreateObject(cx, arr); + return arr; +} + +static SharedShape* GetArrayShapeWithProto(JSContext* cx, HandleObject proto) { + // Get a shape with zero fixed slots, because arrays store the ObjectElements + // header inline. + Rooted<SharedShape*> shape( + cx, SharedShape::getInitialShape(cx, &ArrayObject::class_, cx->realm(), + TaggedProto(proto), /* nfixed = */ 0)); + if (!shape) { + return nullptr; + } + + // Add the |length| property and use the new shape as initial shape for new + // arrays. + if (shape->propMapLength() == 0) { + shape = AddLengthProperty(cx, shape); + if (!shape) { + return nullptr; + } + SharedShape::insertInitialShape(cx, shape); + } else { + MOZ_ASSERT(shape->propMapLength() == 1); + MOZ_ASSERT(shape->lastProperty().key() == NameToId(cx->names().length)); + } + + return shape; +} + +SharedShape* GlobalObject::createArrayShapeWithDefaultProto(JSContext* cx) { + MOZ_ASSERT(!cx->global()->data().arrayShapeWithDefaultProto); + + RootedObject proto(cx, + GlobalObject::getOrCreateArrayPrototype(cx, cx->global())); + if (!proto) { + return nullptr; + } + + SharedShape* shape = GetArrayShapeWithProto(cx, proto); + if (!shape) { + return nullptr; + } + + cx->global()->data().arrayShapeWithDefaultProto.init(shape); + return shape; +} + +template <uint32_t maxLength> +static MOZ_ALWAYS_INLINE ArrayObject* NewArray(JSContext* cx, uint32_t length, + NewObjectKind newKind, + gc::AllocSite* site = nullptr) { + Rooted<SharedShape*> shape(cx, + GlobalObject::getArrayShapeWithDefaultProto(cx)); + if (!shape) { + return nullptr; + } + + return NewArrayWithShape<maxLength>(cx, shape, length, newKind, site); +} + +template <uint32_t maxLength> +static MOZ_ALWAYS_INLINE ArrayObject* NewArrayWithProto(JSContext* cx, + uint32_t length, + HandleObject proto, + NewObjectKind newKind) { + Rooted<SharedShape*> shape(cx); + if (!proto || proto == cx->global()->maybeGetArrayPrototype()) { + shape = GlobalObject::getArrayShapeWithDefaultProto(cx); + } else { + shape = GetArrayShapeWithProto(cx, proto); + } + if (!shape) { + return nullptr; + } + + return NewArrayWithShape<maxLength>(cx, shape, length, newKind, nullptr); +} + +static JSObject* CreateArrayPrototype(JSContext* cx, JSProtoKey key) { + MOZ_ASSERT(key == JSProto_Array); + RootedObject proto(cx, &cx->global()->getObjectPrototype()); + return NewArrayWithProto<0>(cx, 0, proto, TenuredObject); +} + +static bool array_proto_finish(JSContext* cx, JS::HandleObject ctor, + JS::HandleObject proto) { + // Add Array.prototype[@@unscopables]. ECMA-262 draft (2016 Mar 19) 22.1.3.32. + RootedObject unscopables(cx, + NewPlainObjectWithProto(cx, nullptr, TenuredObject)); + if (!unscopables) { + return false; + } + + RootedValue value(cx, BooleanValue(true)); + if (!DefineDataProperty(cx, unscopables, cx->names().at, value) || + !DefineDataProperty(cx, unscopables, cx->names().copyWithin, value) || + !DefineDataProperty(cx, unscopables, cx->names().entries, value) || + !DefineDataProperty(cx, unscopables, cx->names().fill, value) || + !DefineDataProperty(cx, unscopables, cx->names().find, value) || + !DefineDataProperty(cx, unscopables, cx->names().findIndex, value) || + !DefineDataProperty(cx, unscopables, cx->names().findLast, value) || + !DefineDataProperty(cx, unscopables, cx->names().findLastIndex, value) || + !DefineDataProperty(cx, unscopables, cx->names().flat, value) || + !DefineDataProperty(cx, unscopables, cx->names().flatMap, value) || + !DefineDataProperty(cx, unscopables, cx->names().includes, value) || + !DefineDataProperty(cx, unscopables, cx->names().keys, value) || + !DefineDataProperty(cx, unscopables, cx->names().values, value)) { + return false; + } + +#ifdef NIGHTLY_BUILD + if (cx->realm()->creationOptions().getArrayGroupingEnabled()) { + if (!DefineDataProperty(cx, unscopables, cx->names().group, value) || + !DefineDataProperty(cx, unscopables, cx->names().groupToMap, value)) { + return false; + } + } +#endif + + // FIXME: Once bug 1826643 is fixed, the names should be moved into the first + // "or" clause in this method so that they will be alphabetized. + if (cx->realm()->creationOptions().getChangeArrayByCopyEnabled()) { + /* The reason that "with" is not included in the unscopableList is + * because it is already a reserved word. + */ + if (!DefineDataProperty(cx, unscopables, cx->names().toReversed, value) || + !DefineDataProperty(cx, unscopables, cx->names().toSorted, value) || + !DefineDataProperty(cx, unscopables, cx->names().toSpliced, value)) { + return false; + } + } + + RootedId id(cx, PropertyKey::Symbol(cx->wellKnownSymbols().unscopables)); + value.setObject(*unscopables); + return DefineDataProperty(cx, proto, id, value, JSPROP_READONLY); +} + +static const JSClassOps ArrayObjectClassOps = { + array_addProperty, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +static const ClassSpec ArrayObjectClassSpec = { + GenericCreateConstructor<ArrayConstructor, 1, gc::AllocKind::FUNCTION, + &jit::JitInfo_Array>, + CreateArrayPrototype, + array_static_methods, + array_static_props, + array_methods, + nullptr, + array_proto_finish}; + +const JSClass ArrayObject::class_ = { + "Array", + JSCLASS_HAS_CACHED_PROTO(JSProto_Array) | JSCLASS_DELAY_METADATA_BUILDER, + &ArrayObjectClassOps, &ArrayObjectClassSpec}; + +ArrayObject* js::NewDenseEmptyArray(JSContext* cx) { + return NewArray<0>(cx, 0, GenericObject); +} + +ArrayObject* js::NewTenuredDenseEmptyArray(JSContext* cx) { + return NewArray<0>(cx, 0, TenuredObject); +} + +ArrayObject* js::NewDenseFullyAllocatedArray( + JSContext* cx, uint32_t length, NewObjectKind newKind /* = GenericObject */, + gc::AllocSite* site /* = nullptr */) { + return NewArray<UINT32_MAX>(cx, length, newKind, site); +} + +ArrayObject* js::NewDensePartlyAllocatedArray( + JSContext* cx, uint32_t length, + NewObjectKind newKind /* = GenericObject */) { + return NewArray<ArrayObject::EagerAllocationMaxLength>(cx, length, newKind); +} + +ArrayObject* js::NewDensePartlyAllocatedArrayWithProto(JSContext* cx, + uint32_t length, + HandleObject proto) { + return NewArrayWithProto<ArrayObject::EagerAllocationMaxLength>( + cx, length, proto, GenericObject); +} + +ArrayObject* js::NewDenseUnallocatedArray( + JSContext* cx, uint32_t length, + NewObjectKind newKind /* = GenericObject */) { + return NewArray<0>(cx, length, newKind); +} + +// values must point at already-rooted Value objects +ArrayObject* js::NewDenseCopiedArray( + JSContext* cx, uint32_t length, const Value* values, + NewObjectKind newKind /* = GenericObject */) { + ArrayObject* arr = NewArray<UINT32_MAX>(cx, length, newKind); + if (!arr) { + return nullptr; + } + + arr->initDenseElements(values, length); + return arr; +} + +ArrayObject* js::NewDenseCopiedArrayWithProto(JSContext* cx, uint32_t length, + const Value* values, + HandleObject proto) { + ArrayObject* arr = + NewArrayWithProto<UINT32_MAX>(cx, length, proto, GenericObject); + if (!arr) { + return nullptr; + } + + arr->initDenseElements(values, length); + return arr; +} + +ArrayObject* js::NewDenseFullyAllocatedArrayWithTemplate( + JSContext* cx, uint32_t length, ArrayObject* templateObject) { + AutoSetNewObjectMetadata metadata(cx); + gc::AllocKind allocKind = GuessArrayGCKind(length); + MOZ_ASSERT(CanChangeToBackgroundAllocKind(allocKind, &ArrayObject::class_)); + allocKind = ForegroundToBackgroundAllocKind(allocKind); + + Rooted<SharedShape*> shape(cx, templateObject->sharedShape()); + + gc::Heap heap = GetInitialHeap(GenericObject, &ArrayObject::class_); + ArrayObject* arr = ArrayObject::create(cx, allocKind, heap, shape, length, + shape->slotSpan(), metadata); + if (!arr) { + return nullptr; + } + + if (!EnsureNewArrayElements(cx, arr, length)) { + return nullptr; + } + + probes::CreateObject(cx, arr); + + return arr; +} + +// TODO(no-TI): clean up. +ArrayObject* js::NewArrayWithShape(JSContext* cx, uint32_t length, + Handle<Shape*> shape) { + // Ion can call this with a shape from a different realm when calling + // another realm's Array constructor. + Maybe<AutoRealm> ar; + if (cx->realm() != shape->realm()) { + MOZ_ASSERT(cx->compartment() == shape->compartment()); + ar.emplace(cx, shape); + } + + return NewDenseFullyAllocatedArray(cx, length); +} + +#ifdef DEBUG +bool js::ArrayInfo(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject obj(cx); + + for (unsigned i = 0; i < args.length(); i++) { + HandleValue arg = args[i]; + + UniqueChars bytes = + DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, arg, nullptr); + if (!bytes) { + return false; + } + if (arg.isPrimitive() || !(obj = arg.toObjectOrNull())->is<ArrayObject>()) { + fprintf(stderr, "%s: not array\n", bytes.get()); + continue; + } + fprintf(stderr, "%s: (len %u", bytes.get(), + obj->as<ArrayObject>().length()); + fprintf(stderr, ", capacity %u", obj->as<ArrayObject>().getDenseCapacity()); + fputs(")\n", stderr); + } + + args.rval().setUndefined(); + return true; +} +#endif + +void js::ArraySpeciesLookup::initialize(JSContext* cx) { + MOZ_ASSERT(state_ == State::Uninitialized); + + // Get the canonical Array.prototype. + NativeObject* arrayProto = cx->global()->maybeGetArrayPrototype(); + + // Leave the cache uninitialized if the Array class itself is not yet + // initialized. + if (!arrayProto) { + return; + } + + // Get the canonical Array constructor. The Array constructor must be + // initialized if Array.prototype is initialized. + JSObject& arrayCtorObject = cx->global()->getConstructor(JSProto_Array); + JSFunction* arrayCtor = &arrayCtorObject.as<JSFunction>(); + + // Shortcut returns below means Array[@@species] will never be + // optimizable, set to disabled now, and clear it later when we succeed. + state_ = State::Disabled; + + // Look up Array.prototype[@@iterator] and ensure it's a data property. + Maybe<PropertyInfo> ctorProp = + arrayProto->lookup(cx, NameToId(cx->names().constructor)); + if (ctorProp.isNothing() || !ctorProp->isDataProperty()) { + return; + } + + // Get the referred value, and ensure it holds the canonical Array + // constructor. + JSFunction* ctorFun; + if (!IsFunctionObject(arrayProto->getSlot(ctorProp->slot()), &ctorFun)) { + return; + } + if (ctorFun != arrayCtor) { + return; + } + + // Look up the '@@species' value on Array + Maybe<PropertyInfo> speciesProp = arrayCtor->lookup( + cx, PropertyKey::Symbol(cx->wellKnownSymbols().species)); + if (speciesProp.isNothing() || !arrayCtor->hasGetter(*speciesProp)) { + return; + } + + // Get the referred value, ensure it holds the canonical Array[@@species] + // function. + uint32_t speciesGetterSlot = speciesProp->slot(); + JSObject* speciesGetter = arrayCtor->getGetter(speciesGetterSlot); + if (!speciesGetter || !speciesGetter->is<JSFunction>()) { + return; + } + JSFunction* speciesFun = &speciesGetter->as<JSFunction>(); + if (!IsSelfHostedFunctionWithName(speciesFun, cx->names().ArraySpecies)) { + return; + } + + // Store raw pointers below. This is okay to do here, because all objects + // are in the tenured heap. + MOZ_ASSERT(!IsInsideNursery(arrayProto)); + MOZ_ASSERT(!IsInsideNursery(arrayCtor)); + MOZ_ASSERT(!IsInsideNursery(arrayCtor->shape())); + MOZ_ASSERT(!IsInsideNursery(speciesFun)); + MOZ_ASSERT(!IsInsideNursery(arrayProto->shape())); + + state_ = State::Initialized; + arrayProto_ = arrayProto; + arrayConstructor_ = arrayCtor; + arrayConstructorShape_ = arrayCtor->shape(); + arraySpeciesGetterSlot_ = speciesGetterSlot; + canonicalSpeciesFunc_ = speciesFun; + arrayProtoShape_ = arrayProto->shape(); + arrayProtoConstructorSlot_ = ctorProp->slot(); +} + +void js::ArraySpeciesLookup::reset() { + AlwaysPoison(this, JS_RESET_VALUE_PATTERN, sizeof(*this), + MemCheckKind::MakeUndefined); + state_ = State::Uninitialized; +} + +bool js::ArraySpeciesLookup::isArrayStateStillSane() { + MOZ_ASSERT(state_ == State::Initialized); + + // Ensure that Array.prototype still has the expected shape. + if (arrayProto_->shape() != arrayProtoShape_) { + return false; + } + + // Ensure that Array.prototype.constructor contains the canonical Array + // constructor function. + if (arrayProto_->getSlot(arrayProtoConstructorSlot_) != + ObjectValue(*arrayConstructor_)) { + return false; + } + + // Ensure that Array still has the expected shape. + if (arrayConstructor_->shape() != arrayConstructorShape_) { + return false; + } + + // Ensure the species getter contains the canonical @@species function. + JSObject* getter = arrayConstructor_->getGetter(arraySpeciesGetterSlot_); + return getter == canonicalSpeciesFunc_; +} + +bool js::ArraySpeciesLookup::tryOptimizeArray(JSContext* cx, + ArrayObject* array) { + if (state_ == State::Uninitialized) { + // If the cache is not initialized, initialize it. + initialize(cx); + } else if (state_ == State::Initialized && !isArrayStateStillSane()) { + // Otherwise, if the array state is no longer sane, reinitialize. + reset(); + initialize(cx); + } + + // If the cache is disabled or still uninitialized, don't bother trying to + // optimize. + if (state_ != State::Initialized) { + return false; + } + + // By the time we get here, we should have a sane array state. + MOZ_ASSERT(isArrayStateStillSane()); + + // Ensure |array|'s prototype is the actual Array.prototype. + if (array->staticPrototype() != arrayProto_) { + return false; + } + + // Ensure the array does not define an own "constructor" property which may + // shadow `Array.prototype.constructor`. + + // Most arrays don't define any additional own properties beside their + // "length" property. If "length" is the last property, it must be the only + // property, because it's non-configurable. + MOZ_ASSERT(array->shape()->propMapLength() > 0); + PropertyKey lengthKey = NameToId(cx->names().length); + if (MOZ_LIKELY(array->getLastProperty().key() == lengthKey)) { + MOZ_ASSERT(array->shape()->propMapLength() == 1, "Expected one property"); + return true; + } + + // Fail if the array has an own "constructor" property. + uint32_t index; + if (array->shape()->lookup(cx, NameToId(cx->names().constructor), &index)) { + return false; + } + + return true; +} + +JS_PUBLIC_API JSObject* JS::NewArrayObject(JSContext* cx, + const HandleValueArray& contents) { + MOZ_ASSERT(!cx->zone()->isAtomsZone()); + AssertHeapIsIdle(); + CHECK_THREAD(cx); + cx->check(contents); + + return NewDenseCopiedArray(cx, contents.length(), contents.begin()); +} + +JS_PUBLIC_API JSObject* JS::NewArrayObject(JSContext* cx, size_t length) { + MOZ_ASSERT(!cx->zone()->isAtomsZone()); + AssertHeapIsIdle(); + CHECK_THREAD(cx); + + return NewDenseFullyAllocatedArray(cx, length); +} + +JS_PUBLIC_API bool JS::IsArrayObject(JSContext* cx, Handle<JSObject*> obj, + bool* isArray) { + return IsGivenTypeObject(cx, obj, ESClass::Array, isArray); +} + +JS_PUBLIC_API bool JS::IsArrayObject(JSContext* cx, Handle<Value> value, + bool* isArray) { + if (!value.isObject()) { + *isArray = false; + return true; + } + + Rooted<JSObject*> obj(cx, &value.toObject()); + return IsArrayObject(cx, obj, isArray); +} + +JS_PUBLIC_API bool JS::GetArrayLength(JSContext* cx, Handle<JSObject*> obj, + uint32_t* lengthp) { + AssertHeapIsIdle(); + CHECK_THREAD(cx); + cx->check(obj); + + uint64_t len = 0; + if (!GetLengthProperty(cx, obj, &len)) { + return false; + } + + if (len > UINT32_MAX) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_ARRAY_LENGTH); + return false; + } + + *lengthp = uint32_t(len); + return true; +} + +JS_PUBLIC_API bool JS::SetArrayLength(JSContext* cx, Handle<JSObject*> obj, + uint32_t length) { + AssertHeapIsIdle(); + CHECK_THREAD(cx); + cx->check(obj); + + return SetLengthProperty(cx, obj, length); +} + +ArrayObject* js::NewArrayWithNullProto(JSContext* cx) { + Rooted<SharedShape*> shape(cx, GetArrayShapeWithProto(cx, nullptr)); + if (!shape) { + return nullptr; + } + + uint32_t length = 0; + return ::NewArrayWithShape<0>(cx, shape, length, GenericObject); +} diff --git a/js/src/builtin/Array.h b/js/src/builtin/Array.h new file mode 100644 index 0000000000..2e86d70e8c --- /dev/null +++ b/js/src/builtin/Array.h @@ -0,0 +1,247 @@ +/* -*- 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/. */ + +/* JS Array interface. */ + +#ifndef builtin_Array_h +#define builtin_Array_h + +#include "mozilla/Attributes.h" + +#include "vm/JSObject.h" + +namespace js { + +class ArrayObject; + +MOZ_ALWAYS_INLINE bool IdIsIndex(jsid id, uint32_t* indexp) { + if (id.isInt()) { + int32_t i = id.toInt(); + MOZ_ASSERT(i >= 0); + *indexp = uint32_t(i); + return true; + } + + if (MOZ_UNLIKELY(!id.isAtom())) { + return false; + } + + JSAtom* atom = id.toAtom(); + return atom->isIndex(indexp); +} + +// The methods below only create dense boxed arrays. + +// Create a dense array with no capacity allocated, length set to 0, in the +// normal (i.e. non-tenured) heap. +extern ArrayObject* NewDenseEmptyArray(JSContext* cx); + +// Create a dense array with no capacity allocated, length set to 0, in the +// tenured heap. +extern ArrayObject* NewTenuredDenseEmptyArray(JSContext* cx); + +// Create a dense array with a set length, but without allocating space for the +// contents. This is useful, e.g., when accepting length from the user. +extern ArrayObject* NewDenseUnallocatedArray( + JSContext* cx, uint32_t length, NewObjectKind newKind = GenericObject); + +// Create a dense array with length and capacity == 'length', initialized length +// set to 0. +extern ArrayObject* NewDenseFullyAllocatedArray( + JSContext* cx, uint32_t length, NewObjectKind newKind = GenericObject, + gc::AllocSite* site = nullptr); + +// Create a dense array with length == 'length', initialized length set to 0, +// and capacity == 'length' clamped to EagerAllocationMaxLength. +extern ArrayObject* NewDensePartlyAllocatedArray( + JSContext* cx, uint32_t length, NewObjectKind newKind = GenericObject); + +// Like NewDensePartlyAllocatedArray, but the array will have |proto| as +// prototype (or Array.prototype if |proto| is nullptr). +extern ArrayObject* NewDensePartlyAllocatedArrayWithProto(JSContext* cx, + uint32_t length, + HandleObject proto); + +// Create a dense array from the given array values, which must be rooted. +extern ArrayObject* NewDenseCopiedArray(JSContext* cx, uint32_t length, + const Value* values, + NewObjectKind newKind = GenericObject); + +// Like NewDenseCopiedArray, but the array will have |proto| as prototype (or +// Array.prototype if |proto| is nullptr). +extern ArrayObject* NewDenseCopiedArrayWithProto(JSContext* cx, uint32_t length, + const Value* values, + HandleObject proto); + +// Create a dense array based on templateObject with the given length. +extern ArrayObject* NewDenseFullyAllocatedArrayWithTemplate( + JSContext* cx, uint32_t length, ArrayObject* templateObject); + +extern ArrayObject* NewArrayWithShape(JSContext* cx, uint32_t length, + Handle<Shape*> shape); + +extern bool ToLength(JSContext* cx, HandleValue v, uint64_t* out); + +extern bool GetLengthProperty(JSContext* cx, HandleObject obj, + uint64_t* lengthp); + +extern bool SetLengthProperty(JSContext* cx, HandleObject obj, uint32_t length); + +/* + * Copy 'length' elements from aobj to vp. + * + * This function assumes 'length' is effectively the result of calling + * GetLengthProperty on aobj. vp must point to rooted memory. + */ +extern bool GetElements(JSContext* cx, HandleObject aobj, uint32_t length, + js::Value* vp); + +/* Natives exposed for optimization by the interpreter and JITs. */ + +extern bool intrinsic_ArrayNativeSort(JSContext* cx, unsigned argc, + js::Value* vp); + +extern bool array_includes(JSContext* cx, unsigned argc, js::Value* vp); +extern bool array_indexOf(JSContext* cx, unsigned argc, js::Value* vp); +extern bool array_lastIndexOf(JSContext* cx, unsigned argc, js::Value* vp); + +extern bool array_pop(JSContext* cx, unsigned argc, js::Value* vp); + +extern bool array_join(JSContext* cx, unsigned argc, js::Value* vp); + +extern void ArrayShiftMoveElements(ArrayObject* arr); + +extern JSObject* ArraySliceDense(JSContext* cx, HandleObject obj, int32_t begin, + int32_t end, HandleObject result); + +extern JSObject* ArgumentsSliceDense(JSContext* cx, HandleObject obj, + int32_t begin, int32_t end, + HandleObject result); + +extern ArrayObject* NewArrayWithNullProto(JSContext* cx); + +/* + * Append the given (non-hole) value to the end of an array. The array must be + * a newborn array -- that is, one which has not been exposed to script for + * arbitrary manipulation. (This method optimizes on the assumption that + * extending the array to accommodate the element will never make the array + * sparse, which requires that the array be completely filled.) + */ +extern bool NewbornArrayPush(JSContext* cx, HandleObject obj, const Value& v); + +extern ArrayObject* ArrayConstructorOneArg(JSContext* cx, + Handle<ArrayObject*> templateObject, + int32_t lengthInt); + +#ifdef DEBUG +extern bool ArrayInfo(JSContext* cx, unsigned argc, Value* vp); +#endif + +/* Array constructor native. Exposed only so the JIT can know its address. */ +extern bool ArrayConstructor(JSContext* cx, unsigned argc, Value* vp); + +// Like Array constructor, but doesn't perform GetPrototypeFromConstructor. +extern bool array_construct(JSContext* cx, unsigned argc, Value* vp); + +extern JSString* ArrayToSource(JSContext* cx, HandleObject obj); + +extern bool IsCrossRealmArrayConstructor(JSContext* cx, JSObject* obj, + bool* result); + +extern bool PrototypeMayHaveIndexedProperties(NativeObject* obj); + +// JS::IsArray has multiple overloads, use js::IsArrayFromJit to disambiguate. +extern bool IsArrayFromJit(JSContext* cx, HandleObject obj, bool* isArray); + +extern bool ArrayLengthGetter(JSContext* cx, HandleObject obj, HandleId id, + MutableHandleValue vp); + +extern bool ArrayLengthSetter(JSContext* cx, HandleObject obj, HandleId id, + HandleValue v, ObjectOpResult& result); + +class MOZ_NON_TEMPORARY_CLASS ArraySpeciesLookup final { + /* + * An ArraySpeciesLookup holds the following: + * + * Array.prototype (arrayProto_) + * To ensure that the incoming array has the standard proto. + * + * Array.prototype's shape (arrayProtoShape_) + * To ensure that Array.prototype has not been modified. + * + * Array (arrayConstructor_) + * Array's shape (arrayConstructorShape_) + * To ensure that Array has not been modified. + * + * Array.prototype's slot number for constructor (arrayProtoConstructorSlot_) + * To quickly retrieve and ensure that the Array constructor + * stored in the slot has not changed. + * + * Array's slot number for the @@species getter. (arraySpeciesGetterSlot_) + * Array's canonical value for @@species (canonicalSpeciesFunc_) + * To quickly retrieve and ensure that the @@species getter for Array + * has not changed. + * + * MOZ_INIT_OUTSIDE_CTOR fields below are set in |initialize()|. The + * constructor only initializes a |state_| field, that defines whether the + * other fields are accessible. + */ + + // Pointer to canonical Array.prototype and Array. + MOZ_INIT_OUTSIDE_CTOR NativeObject* arrayProto_; + MOZ_INIT_OUTSIDE_CTOR NativeObject* arrayConstructor_; + + // Shape of matching Array, and slot containing the @@species property, and + // the canonical value. + MOZ_INIT_OUTSIDE_CTOR Shape* arrayConstructorShape_; + MOZ_INIT_OUTSIDE_CTOR uint32_t arraySpeciesGetterSlot_; + MOZ_INIT_OUTSIDE_CTOR JSFunction* canonicalSpeciesFunc_; + + // Shape of matching Array.prototype object, and slot containing the + // constructor for it. + MOZ_INIT_OUTSIDE_CTOR Shape* arrayProtoShape_; + MOZ_INIT_OUTSIDE_CTOR uint32_t arrayProtoConstructorSlot_; + + enum class State : uint8_t { + // Flags marking the lazy initialization of the above fields. + Uninitialized, + Initialized, + + // The disabled flag is set when we don't want to try optimizing + // anymore because core objects were changed. + Disabled + }; + + State state_ = State::Uninitialized; + + // Initialize the internal fields. + void initialize(JSContext* cx); + + // Reset the cache. + void reset(); + + // Check if the global array-related objects have not been messed with + // in a way that would disable this cache. + bool isArrayStateStillSane(); + + public: + /** Construct an |ArraySpeciesLookup| in the uninitialized state. */ + ArraySpeciesLookup() { reset(); } + + // Try to optimize the @@species lookup for an array. + bool tryOptimizeArray(JSContext* cx, ArrayObject* array); + + // Purge the cache and all info associated with it. + void purge() { + if (state_ == State::Initialized) { + reset(); + } + } +}; + +} /* namespace js */ + +#endif /* builtin_Array_h */ diff --git a/js/src/builtin/Array.js b/js/src/builtin/Array.js new file mode 100644 index 0000000000..4544db3771 --- /dev/null +++ b/js/src/builtin/Array.js @@ -0,0 +1,1561 @@ +/* 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/. */ + +/* ES5 15.4.4.16. */ +function ArrayEvery(callbackfn /*, thisArg*/) { + /* Step 1. */ + var O = ToObject(this); + + /* Steps 2-3. */ + var len = ToLength(O.length); + + /* Step 4. */ + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Array.prototype.every"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + /* Step 5. */ + var T = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + /* Steps 6-7. */ + /* Steps a (implicit), and d. */ + for (var k = 0; k < len; k++) { + /* Step b */ + if (k in O) { + /* Step c. */ + if (!callContentFunction(callbackfn, T, O[k], k, O)) { + return false; + } + } + } + + /* Step 8. */ + return true; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(ArrayEvery); + +/* ES5 15.4.4.17. */ +function ArraySome(callbackfn /*, thisArg*/) { + /* Step 1. */ + var O = ToObject(this); + + /* Steps 2-3. */ + var len = ToLength(O.length); + + /* Step 4. */ + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Array.prototype.some"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + /* Step 5. */ + var T = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + /* Steps 6-7. */ + /* Steps a (implicit), and d. */ + for (var k = 0; k < len; k++) { + /* Step b */ + if (k in O) { + /* Step c. */ + if (callContentFunction(callbackfn, T, O[k], k, O)) { + return true; + } + } + } + + /* Step 8. */ + return false; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(ArraySome); + +// ES2023 draft rev cb4224156c54156f30c18c50784c1b0148ebfae5 +// 23.1.3.30 Array.prototype.sort ( comparefn ) +function ArraySortCompare(comparefn) { + return function(x, y) { + // Steps 4.a-c. + if (x === undefined) { + if (y === undefined) { + return 0; + } + return 1; + } + if (y === undefined) { + return -1; + } + + // Step 4.d.i. + var v = ToNumber(callContentFunction(comparefn, undefined, x, y)); + + // Steps 4.d.ii-iii. + return v !== v ? 0 : v; + }; +} + +// ES2023 draft rev cb4224156c54156f30c18c50784c1b0148ebfae5 +// 23.1.3.30 Array.prototype.sort ( comparefn ) +function ArraySort(comparefn) { + // Step 1. + if (comparefn !== undefined) { + if (!IsCallable(comparefn)) { + ThrowTypeError(JSMSG_BAD_SORT_ARG); + } + } + + // Step 2. + var O = ToObject(this); + + // First try to sort the array in native code, if that fails, indicated by + // returning |false| from ArrayNativeSort, sort it in self-hosted code. + if (callFunction(ArrayNativeSort, O, comparefn)) { + return O; + } + + // Step 3. + var len = ToLength(O.length); + + // Arrays with less than two elements remain unchanged when sorted. + if (len <= 1) { + return O; + } + + // Step 4. + var wrappedCompareFn = ArraySortCompare(comparefn); + + // Step 5. + // To save effort we will do all of our work on a dense list, then create + // holes at the end. + var denseList = []; + var denseLen = 0; + + for (var i = 0; i < len; i++) { + if (i in O) { + DefineDataProperty(denseList, denseLen++, O[i]); + } + } + + if (denseLen < 1) { + return O; + } + + var sorted = MergeSort(denseList, denseLen, wrappedCompareFn); + + assert(IsPackedArray(sorted), "sorted is a packed array"); + assert(sorted.length === denseLen, "sorted array has the correct length"); + + MoveHoles(O, len, sorted, denseLen); + + return O; +} + +/* ES5 15.4.4.18. */ +function ArrayForEach(callbackfn /*, thisArg*/) { + /* Step 1. */ + var O = ToObject(this); + + /* Steps 2-3. */ + var len = ToLength(O.length); + + /* Step 4. */ + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Array.prototype.forEach"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + /* Step 5. */ + var T = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + /* Steps 6-7. */ + /* Steps a (implicit), and d. */ + for (var k = 0; k < len; k++) { + /* Step b */ + if (k in O) { + /* Step c. */ + callContentFunction(callbackfn, T, O[k], k, O); + } + } + + /* Step 8. */ + return undefined; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(ArrayForEach); + +/* ES 2016 draft Mar 25, 2016 22.1.3.15. */ +function ArrayMap(callbackfn /*, thisArg*/) { + /* Step 1. */ + var O = ToObject(this); + + /* Step 2. */ + var len = ToLength(O.length); + + /* Step 3. */ + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Array.prototype.map"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + /* Step 4. */ + var T = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + /* Steps 5. */ + var A = ArraySpeciesCreate(O, len); + + /* Steps 6-7. */ + /* Steps 7.a (implicit), and 7.d. */ + for (var k = 0; k < len; k++) { + /* Steps 7.b-c. */ + if (k in O) { + /* Steps 7.c.i-iii. */ + var mappedValue = callContentFunction(callbackfn, T, O[k], k, O); + DefineDataProperty(A, k, mappedValue); + } + } + + /* Step 8. */ + return A; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(ArrayMap); + +/* ES 2016 draft Mar 25, 2016 22.1.3.7 Array.prototype.filter. */ +function ArrayFilter(callbackfn /*, thisArg*/) { + /* Step 1. */ + var O = ToObject(this); + + /* Step 2. */ + var len = ToLength(O.length); + + /* Step 3. */ + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Array.prototype.filter"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + /* Step 4. */ + var T = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + /* Step 5. */ + var A = ArraySpeciesCreate(O, 0); + + /* Steps 6-8. */ + /* Steps 8.a (implicit), and 8.d. */ + for (var k = 0, to = 0; k < len; k++) { + /* Steps 8.b-c. */ + if (k in O) { + /* Step 8.c.i. */ + var kValue = O[k]; + /* Steps 8.c.ii-iii. */ + if (callContentFunction(callbackfn, T, kValue, k, O)) { + DefineDataProperty(A, to++, kValue); + } + } + } + + /* Step 9. */ + return A; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(ArrayFilter); + +#ifdef NIGHTLY_BUILD +// Array Grouping proposal +// +// Array.prototype.group +// https://tc39.es/proposal-array-grouping/#sec-array.prototype.group +function ArrayGroup(callbackfn /*, thisArg*/) { + /* Step 1. Let O be ? ToObject(this value). */ + var O = ToObject(this); + + /* Step 2. Let len be ? LengthOfArrayLike(O). */ + var len = ToLength(O.length); + + /* Step 3. If IsCallable(callbackfn) is false, throw a TypeError exception. */ + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + /* Step 5. Let groups be a new empty List. */ + // Not applicable in our implementation. + + /* Step 7. Let obj be ! OrdinaryObjectCreate(null). */ + var object = std_Object_create(null); + + var thisArg = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + /* Steps 4, 6. */ + for (var k = 0; k < len; k++) { + /* Skip Step 6.a. Let Pk be ! ToString(𝔽(k)). + * + * k is coerced into a string through the property access. */ + + /* Step 6.b. Let kValue be ? Get(O, Pk). */ + var kValue = O[k]; + + /* Step 6.c. + * Let propertyKey be ? ToPropertyKey( + * ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »)). + */ + var propertyKey = callContentFunction(callbackfn, thisArg, kValue, k, O); + + // Split the step to ensure single evaluation in the TO_PROPERTY_KEY macro. + propertyKey = TO_PROPERTY_KEY(propertyKey); + + /* Step 6.d. Perform ! AddValueToKeyedGroup(groups, propertyKey, kValue). */ + var elements = object[propertyKey]; + if (elements === undefined) { + DefineDataProperty(object, propertyKey, [kValue]); + } else { + DefineDataProperty(elements, elements.length, kValue); + } + } + + /* Step 8. For each Record { [[Key]], [[Elements]] } g of groups, do + * a. Let elements be ! CreateArrayFromList(g.[[Elements]]). + * b. Perform ! CreateDataPropertyOrThrow(obj, g.[[Key]], elements). + */ + // Not applicable in our implementation. + + /* Step 9. Return obj. */ + return object; +} + +// Array Grouping proposal +// +// Array.prototype.groupToMap +// https://tc39.es/proposal-array-grouping/#sec-array.prototype.grouptomap +function ArrayGroupToMap(callbackfn /*, thisArg*/) { + /* Step 1. Let O be ? ToObject(this value). */ + var O = ToObject(this); + + /* Step 2. Let len be ? LengthOfArrayLike(O). */ + var len = ToLength(O.length); + + /* Step 3. + * If IsCallable(callbackfn) is false, throw a TypeError exception. + */ + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + /* Skipping Step 5. Let groups be a new empty List. + * + * Intermediate object isn't necessary as we have direct access + * to the map constructor and set/get methods. + */ + + /* Step 7. Let map be ! Construct(%Map%). */ + var C = GetBuiltinConstructor("Map"); + var map = new C(); + + var thisArg = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + /* Combine Step 6. and Step 8. + * + * We have direct access to the map constructor and set/get methods. + * We can treat these two loops as one, as there isn't a risk that user + * polyfilling will impact the implementation. + */ + for (var k = 0; k < len; k++) { + /* Skipping Step 6.a. Let Pk be ! ToString(𝔽(k)). + * + * Value is coerced to String by property access in step 6.b. + */ + + /* Step 6.b. Let kValue be ? Get(O, Pk). */ + var kValue = O[k]; + + /* Step 6.c. + * Let key be ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »). + */ + var key = callContentFunction(callbackfn, thisArg, kValue, k, O); + + /* Skipping Step 6.d. If key is -0𝔽, set key to +0𝔽. + * + * This step is performed by std_Map_set. + */ + + /* Step 8.c. Append entry as the last element of map.[[MapData]]. + * + * We are not using an intermediate object to store the values. + * So, this step applies it directly to the map object. Skips steps + * 6.e (Perform ! AddValueToKeyedGroup(groups, key, kValue)) + * and 8.a-b as a result. + */ + var elements = callFunction(std_Map_get, map, key); + if (elements === undefined) { + callFunction(std_Map_set, map, key, [kValue]); + } else { + DefineDataProperty(elements, elements.length, kValue); + } + } + + /* Step 9. Return map. */ + return map; +} + +#endif + +/* ES5 15.4.4.21. */ +function ArrayReduce(callbackfn /*, initialValue*/) { + /* Step 1. */ + var O = ToObject(this); + + /* Steps 2-3. */ + var len = ToLength(O.length); + + /* Step 4. */ + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Array.prototype.reduce"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + /* Step 6. */ + var k = 0; + + /* Steps 5, 7-8. */ + var accumulator; + if (ArgumentsLength() > 1) { + accumulator = GetArgument(1); + } else { + /* Step 5. */ + // Add an explicit |throw| here and below to inform Ion that the + // ThrowTypeError calls exit this function. + if (len === 0) { + throw ThrowTypeError(JSMSG_EMPTY_ARRAY_REDUCE); + } + + // Use a |do-while| loop to let Ion know that the loop will definitely + // be entered at least once. When Ion is then also able to inline the + // |in| operator, it can optimize away the whole loop. + var kPresent = false; + do { + if (k in O) { + kPresent = true; + break; + } + } while (++k < len); + if (!kPresent) { + throw ThrowTypeError(JSMSG_EMPTY_ARRAY_REDUCE); + } + + // Moved outside of the loop to ensure the assignment is non-conditional. + accumulator = O[k++]; + } + + /* Step 9. */ + /* Steps a (implicit), and d. */ + for (; k < len; k++) { + /* Step b */ + if (k in O) { + /* Step c. */ + accumulator = callContentFunction( + callbackfn, + undefined, + accumulator, + O[k], + k, + O + ); + } + } + + /* Step 10. */ + return accumulator; +} + +/* ES5 15.4.4.22. */ +function ArrayReduceRight(callbackfn /*, initialValue*/) { + /* Step 1. */ + var O = ToObject(this); + + /* Steps 2-3. */ + var len = ToLength(O.length); + + /* Step 4. */ + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Array.prototype.reduce"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + /* Step 6. */ + var k = len - 1; + + /* Steps 5, 7-8. */ + var accumulator; + if (ArgumentsLength() > 1) { + accumulator = GetArgument(1); + } else { + /* Step 5. */ + // Add an explicit |throw| here and below to inform Ion that the + // ThrowTypeError calls exit this function. + if (len === 0) { + throw ThrowTypeError(JSMSG_EMPTY_ARRAY_REDUCE); + } + + // Use a |do-while| loop to let Ion know that the loop will definitely + // be entered at least once. When Ion is then also able to inline the + // |in| operator, it can optimize away the whole loop. + var kPresent = false; + do { + if (k in O) { + kPresent = true; + break; + } + } while (--k >= 0); + if (!kPresent) { + throw ThrowTypeError(JSMSG_EMPTY_ARRAY_REDUCE); + } + + // Moved outside of the loop to ensure the assignment is non-conditional. + accumulator = O[k--]; + } + + /* Step 9. */ + /* Steps a (implicit), and d. */ + for (; k >= 0; k--) { + /* Step b */ + if (k in O) { + /* Step c. */ + accumulator = callContentFunction( + callbackfn, + undefined, + accumulator, + O[k], + k, + O + ); + } + } + + /* Step 10. */ + return accumulator; +} + +/* ES6 draft 2013-05-14 15.4.3.23. */ +function ArrayFind(predicate /*, thisArg*/) { + /* Steps 1-2. */ + var O = ToObject(this); + + /* Steps 3-5. */ + var len = ToLength(O.length); + + /* Step 6. */ + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Array.prototype.find"); + } + if (!IsCallable(predicate)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, predicate)); + } + + /* Step 7. */ + var T = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + /* Steps 8-9. */ + /* Steps a (implicit), and g. */ + for (var k = 0; k < len; k++) { + /* Steps a-c. */ + var kValue = O[k]; + /* Steps d-f. */ + if (callContentFunction(predicate, T, kValue, k, O)) { + return kValue; + } + } + + /* Step 10. */ + return undefined; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(ArrayFind); + +/* ES6 draft 2013-05-14 15.4.3.23. */ +function ArrayFindIndex(predicate /*, thisArg*/) { + /* Steps 1-2. */ + var O = ToObject(this); + + /* Steps 3-5. */ + var len = ToLength(O.length); + + /* Step 6. */ + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Array.prototype.find"); + } + if (!IsCallable(predicate)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, predicate)); + } + + /* Step 7. */ + var T = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + /* Steps 8-9. */ + /* Steps a (implicit), and g. */ + for (var k = 0; k < len; k++) { + /* Steps a-f. */ + if (callContentFunction(predicate, T, O[k], k, O)) { + return k; + } + } + + /* Step 10. */ + return -1; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(ArrayFindIndex); + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 22.1.3.3 Array.prototype.copyWithin ( target, start [ , end ] ) +function ArrayCopyWithin(target, start, end = undefined) { + // Step 1. + var O = ToObject(this); + + // Step 2. + var len = ToLength(O.length); + + // Step 3. + var relativeTarget = ToInteger(target); + + // Step 4. + var to = + relativeTarget < 0 + ? std_Math_max(len + relativeTarget, 0) + : std_Math_min(relativeTarget, len); + + // Step 5. + var relativeStart = ToInteger(start); + + // Step 6. + var from = + relativeStart < 0 + ? std_Math_max(len + relativeStart, 0) + : std_Math_min(relativeStart, len); + + // Step 7. + var relativeEnd = end === undefined ? len : ToInteger(end); + + // Step 8. + var final = + relativeEnd < 0 + ? std_Math_max(len + relativeEnd, 0) + : std_Math_min(relativeEnd, len); + + // Step 9. + var count = std_Math_min(final - from, len - to); + + // Steps 10-12. + if (from < to && to < from + count) { + // Steps 10.b-c. + from = from + count - 1; + to = to + count - 1; + + // Step 12. + while (count > 0) { + if (from in O) { + O[to] = O[from]; + } else { + delete O[to]; + } + + from--; + to--; + count--; + } + } else { + // Step 12. + while (count > 0) { + if (from in O) { + O[to] = O[from]; + } else { + delete O[to]; + } + + from++; + to++; + count--; + } + } + + // Step 13. + return O; +} + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 22.1.3.6 Array.prototype.fill ( value [ , start [ , end ] ] ) +function ArrayFill(value, start = 0, end = undefined) { + // Step 1. + var O = ToObject(this); + + // Step 2. + var len = ToLength(O.length); + + // Step 3. + var relativeStart = ToInteger(start); + + // Step 4. + var k = + relativeStart < 0 + ? std_Math_max(len + relativeStart, 0) + : std_Math_min(relativeStart, len); + + // Step 5. + var relativeEnd = end === undefined ? len : ToInteger(end); + + // Step 6. + var final = + relativeEnd < 0 + ? std_Math_max(len + relativeEnd, 0) + : std_Math_min(relativeEnd, len); + + // Step 7. + for (; k < final; k++) { + O[k] = value; + } + + // Step 8. + return O; +} + +// ES6 draft specification, section 22.1.5.1, version 2013-09-05. +function CreateArrayIterator(obj, kind) { + var iteratedObject = ToObject(obj); + var iterator = NewArrayIterator(); + UnsafeSetReservedSlot(iterator, ITERATOR_SLOT_TARGET, iteratedObject); + UnsafeSetReservedSlot(iterator, ITERATOR_SLOT_NEXT_INDEX, 0); + UnsafeSetReservedSlot(iterator, ITERATOR_SLOT_ITEM_KIND, kind); + return iterator; +} + +// ES6, 22.1.5.2.1 +// http://www.ecma-international.org/ecma-262/6.0/index.html#sec-%arrayiteratorprototype%.next +function ArrayIteratorNext() { + // Step 1-3. + var obj = this; + if (!IsObject(obj) || (obj = GuardToArrayIterator(obj)) === null) { + return callFunction( + CallArrayIteratorMethodIfWrapped, + this, + "ArrayIteratorNext" + ); + } + + // Step 4. + var a = UnsafeGetReservedSlot(obj, ITERATOR_SLOT_TARGET); + var result = { value: undefined, done: false }; + + // Step 5. + if (a === null) { + result.done = true; + return result; + } + + // Step 6. + // The index might not be an integer, so we have to do a generic get here. + var index = UnsafeGetReservedSlot(obj, ITERATOR_SLOT_NEXT_INDEX); + + // Step 7. + var itemKind = UnsafeGetInt32FromReservedSlot(obj, ITERATOR_SLOT_ITEM_KIND); + + // Step 8-9. + var len; + if (IsPossiblyWrappedTypedArray(a)) { + len = PossiblyWrappedTypedArrayLength(a); + + // If the length is non-zero, the buffer can't be detached. + if (len === 0) { + if (PossiblyWrappedTypedArrayHasDetachedBuffer(a)) { + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + } + } + } else { + len = ToLength(a.length); + } + + // Step 10. + if (index >= len) { + UnsafeSetReservedSlot(obj, ITERATOR_SLOT_TARGET, null); + result.done = true; + return result; + } + + // Step 11. + UnsafeSetReservedSlot(obj, ITERATOR_SLOT_NEXT_INDEX, index + 1); + + // Step 16. + if (itemKind === ITEM_KIND_VALUE) { + result.value = a[index]; + return result; + } + + // Step 13. + if (itemKind === ITEM_KIND_KEY_AND_VALUE) { + var pair = [index, a[index]]; + result.value = pair; + return result; + } + + // Step 12. + assert(itemKind === ITEM_KIND_KEY, itemKind); + result.value = index; + return result; +} +// We want to inline this to do scalar replacement of the result object. +SetIsInlinableLargeFunction(ArrayIteratorNext); + +// Uncloned functions with `$` prefix are allocated as extended function +// to store the original name in `SetCanonicalName`. +function $ArrayValues() { + return CreateArrayIterator(this, ITEM_KIND_VALUE); +} +SetCanonicalName($ArrayValues, "values"); + +function ArrayEntries() { + return CreateArrayIterator(this, ITEM_KIND_KEY_AND_VALUE); +} + +function ArrayKeys() { + return CreateArrayIterator(this, ITEM_KIND_KEY); +} + +// https://tc39.es/proposal-array-from-async/ +// TODO: Bug 1834560 The step numbers in this will need updating when this is merged +// into the main spec. +function ArrayFromAsync(asyncItems, mapfn = undefined, thisArg = undefined) { + // Step 1. Let C be the this value. + var C = this; + + // Step 2. Let promiseCapability be ! NewPromiseCapability(%Promise%). + // Step 3. Let fromAsyncClosure be a new Abstract Closure with no parameters that captures C, mapfn, and thisArg and performs the following steps when called: + let fromAsyncClosure = async () => { + // Step 3.a. If mapfn is undefined, let mapping be false. + // Step 3.b. Else, + // Step 3.b.i. If IsCallable(mapfn) is false, throw a TypeError exception. + // Step 3.b.ii. Let mapping be true. + var mapping = mapfn !== undefined; + if (mapping && !IsCallable(mapfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, ToSource(mapfn)); + } + + // Step 3.c. Let usingAsyncIterator be ? GetMethod(asyncItems, @@asyncIterator). + let usingAsyncIterator = asyncItems[GetBuiltinSymbol("asyncIterator")]; + if (usingAsyncIterator === null) { + usingAsyncIterator = undefined; + } + + let usingSyncIterator = undefined; + if (usingAsyncIterator !== undefined) { + if (!IsCallable(usingAsyncIterator)) { + ThrowTypeError(JSMSG_NOT_ITERABLE, ToSource(asyncItems)); + } + } else { + // Step 3.d. If usingAsyncIterator is undefined, then + + // Step 3.d.i. Let usingSyncIterator be ? GetMethod(asyncItems, @@iterator). + usingSyncIterator = asyncItems[GetBuiltinSymbol("iterator")]; + if (usingSyncIterator === null) { + usingSyncIterator = undefined; + } + + if (usingSyncIterator !== undefined) { + if (!IsCallable(usingSyncIterator)) { + ThrowTypeError(JSMSG_NOT_ITERABLE, ToSource(asyncItems)); + } + } + } + + // Step 3.g. Let iteratorRecord be undefined. + // Step 3.j. If iteratorRecord is not undefined, then ... + if (usingAsyncIterator !== undefined || usingSyncIterator !== undefined) { + // Note: The published spec as of f6acfc4f0277e625f13fd22068138aec61a12df3 + // is incorrect. See https://github.com/tc39/proposal-array-from-async/issues/33 + // Here we use the implementation provided by @bakkot in that bug + // in lieu for now; This allows to use a for-await loop below. + + // Steps 3.h-i are implicit through the for-await loop. + + // Step 3.h. If usingAsyncIterator is not undefined, then + // Step 3.h.i. Set iteratorRecord to ? GetIterator(asyncItems, async, usingAsyncIterator). + // Step 3.i. Else if usingSyncIterator is not undefined, then + // Set iteratorRecord to ? CreateAsyncFromSyncIterator(GetIterator(asyncItems, sync, usingSyncIterator)). + + // https://github.com/tc39/proposal-array-from-async/pull/41 + // Step 3.e. If IsConstructor(C) is true, then + // Step 3.e.i. Let A be ? Construct(C). + // Step 3.f. Else, + // Step 3.f.i. Let A be ! ArrayCreate(0). + let A = IsConstructor(C) ? constructContentFunction(C, C) : []; + + + // Step 3.j.i. Let k be 0. + let k = 0; + + // Step 3.j.ii. Repeat, + for await (let nextValue of allowContentIterWith( + asyncItems, + usingAsyncIterator, + usingSyncIterator + )) { + // Following in the steps of Array.from, we don't actually implement 3.j.ii.1. + // The comment in Array.from also applies here; we should only encounter this + // after a huge loop around a proxy + // Step 3.j.ii.1. If k ≥ 2**53 - 1, then + // Step 3.j.ii.1.a. Let error be ThrowCompletion(a newly created TypeError object). + // Step 3.j.ii.1.b. Return ? AsyncIteratorClose(iteratorRecord, error). + // Step 3.j.ii.2. Let Pk be ! ToString(𝔽(k)). + + // Step 3.j.ii.3. Let next be ? Await(IteratorStep(iteratorRecord)). + + // Step 3.j.ii.5. Let nextValue be ? IteratorValue(next). (Implicit through the for-await loop). + + // Step 3.j.ii.7. Else, let mappedValue be nextValue. (Reordered) + let mappedValue = nextValue; + + // Step 3.j.ii.6. If mapping is true, then + if (mapping) { + // Step 3.j.ii.6.a. Let mappedValue be Call(mapfn, thisArg, « nextValue, 𝔽(k) »). + // Step 3.j.ii.6.b. IfAbruptCloseAsyncIterator(mappedValue, iteratorRecord). + // Abrupt completion will be handled by the for-await loop. + mappedValue = callContentFunction(mapfn, thisArg, nextValue, k); + + // Step 3.j.ii.6.c. Set mappedValue to Await(mappedValue). + // Step 3.j.ii.6.d. IfAbruptCloseAsyncIterator(mappedValue, iteratorRecord). + mappedValue = await mappedValue; + } + + // Step 3.j.ii.8. Let defineStatus be CreateDataPropertyOrThrow(A, Pk, mappedValue). + // Step 3.j.ii.9. If defineStatus is an abrupt completion, return ? AsyncIteratorClose(iteratorRecord, defineStatus). + DefineDataProperty(A, k, mappedValue); + + // Step 3.j.ii.10. Set k to k + 1. + k = k + 1; + } + + // Step 3.j.ii.4. If next is false, then (Reordered) + + // Step 3.j.ii.4.a. Perform ? Set(A, "length", 𝔽(k), true). + A.length = k; + + // Step 3.j.ii.4.b. Return Completion Record { [[Type]]: return, [[Value]]: A, [[Target]]: empty }. + return A; + } + + // Step 3.k. Else, + + // Step 3.k.i. NOTE: asyncItems is neither an AsyncIterable nor an Iterable so assume it is an array-like object. + // Step 3.k.ii. Let arrayLike be ! ToObject(asyncItems). + let arrayLike = ToObject(asyncItems); + + // Step 3.k.iii. Let len be ? LengthOfArrayLike(arrayLike). + let len = ToLength(arrayLike.length); + + // Step 3.k.iv. If IsConstructor(C) is true, then + // Step 3.k.iv.1. Let A be ? Construct(C, « 𝔽(len) »). + // Step 3.k.v. Else, + // Step 3.k.v.1. Let A be ? ArrayCreate(len). + let A = IsConstructor(C) ? constructContentFunction(C, C, len) : std_Array(len); + + // Step 3.k.vi. Let k be 0. + let k = 0; + + // Step 3.k.vii. Repeat, while k < len, + while (k < len) { + // Step 3.k.vii.1. Let Pk be ! ToString(𝔽(k)). + // Step 3.k.vii.2. Let kValue be ? Get(arrayLike, Pk). + // Step 3.k.vii.3. Let kValue be ? Await(kValue). + let kValue = await arrayLike[k]; + + // Step 3.k.vii.4. If mapping is true, then + // Step 3.k.vii.4.a. Let mappedValue be ? Call(mapfn, thisArg, « kValue, 𝔽(k) »). + // Step 3.k.vii.4.b. Let mappedValue be ? Await(mappedValue). + // Step 3.k.vii.5. Else, let mappedValue be kValue. + let mappedValue = mapping + ? await callContentFunction(mapfn, thisArg, kValue, k) + : kValue; + + // Step 3.k.vii.6. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue). + DefineDataProperty(A, k, mappedValue); + + // Step 3.k.vii.7. Set k to k + 1. + k = k + 1; + } + + // Step 3.k.viii. Perform ? Set(A, "length", 𝔽(len), true). + A.length = len; + + // Step 3.k.ix. Return Completion Record { [[Type]]: return, [[Value]]: A, [[Target]]: empty }. + return A; + }; + + // Step 4. Perform AsyncFunctionStart(promiseCapability, fromAsyncClosure). + // Step 5. Return promiseCapability.[[Promise]]. + return fromAsyncClosure(); +} + +// ES 2017 draft 0f10dba4ad18de92d47d421f378233a2eae8f077 22.1.2.1 +function ArrayFrom(items, mapfn = undefined, thisArg = undefined) { + // Step 1. + var C = this; + + // Steps 2-3. + var mapping = mapfn !== undefined; + if (mapping && !IsCallable(mapfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(1, mapfn)); + } + var T = thisArg; + + // Step 4. + // Inlined: GetMethod, steps 1-2. + var usingIterator = items[GetBuiltinSymbol("iterator")]; + + // Step 5. + // Inlined: GetMethod, step 3. + if (!IsNullOrUndefined(usingIterator)) { + // Inlined: GetMethod, step 4. + if (!IsCallable(usingIterator)) { + ThrowTypeError(JSMSG_NOT_ITERABLE, DecompileArg(0, items)); + } + + // Steps 5.a-b. + var A = IsConstructor(C) ? constructContentFunction(C, C) : []; + + // Step 5.d. + var k = 0; + + // Steps 5.c, 5.e + for (var nextValue of allowContentIterWith(items, usingIterator)) { + // Step 5.e.i. + // Disabled for performance reason. We won't hit this case on + // normal array, since DefineDataProperty will throw before it. + // We could hit this when |A| is a proxy and it ignores + // |DefineDataProperty|, but it happens only after too long loop. + /* + if (k >= 0x1fffffffffffff) { + ThrowTypeError(JSMSG_TOO_LONG_ARRAY); + } + */ + + // Steps 5.e.vi-vii. + var mappedValue = mapping + ? callContentFunction(mapfn, T, nextValue, k) + : nextValue; + + // Steps 5.e.ii (reordered), 5.e.viii. + DefineDataProperty(A, k++, mappedValue); + } + + // Step 5.e.iv. + A.length = k; + return A; + } + + // Step 7 is an assertion: items is not an Iterator. Testing this is + // literally the very last thing we did, so we don't assert here. + + // Steps 8-9. + var arrayLike = ToObject(items); + + // Steps 10-11. + var len = ToLength(arrayLike.length); + + // Steps 12-14. + var A = IsConstructor(C) + ? constructContentFunction(C, C, len) + : std_Array(len); + + // Steps 15-16. + for (var k = 0; k < len; k++) { + // Steps 16.a-c. + var kValue = items[k]; + + // Steps 16.d-e. + var mappedValue = mapping + ? callContentFunction(mapfn, T, kValue, k) + : kValue; + + // Steps 16.f-g. + DefineDataProperty(A, k, mappedValue); + } + + // Steps 17-18. + A.length = len; + + // Step 19. + return A; +} + +// ES2015 22.1.3.27 Array.prototype.toString. +function ArrayToString() { + // Steps 1-2. + var array = ToObject(this); + + // Steps 3-4. + var func = array.join; + + // Steps 5-6. + if (!IsCallable(func)) { + return callFunction(std_Object_toString, array); + } + return callContentFunction(func, array); +} + +// ES2017 draft rev f8a9be8ea4bd97237d176907a1e3080dce20c68f +// 22.1.3.27 Array.prototype.toLocaleString ([ reserved1 [ , reserved2 ] ]) +// ES2017 Intl draft rev 78bbe7d1095f5ff3760ac4017ed366026e4cb276 +// 13.4.1 Array.prototype.toLocaleString ([ locales [ , options ]]) +function ArrayToLocaleString(locales, options) { + // Step 1 (ToObject already performed in native code). + assert(IsObject(this), "|this| should be an object"); + var array = this; + + // Step 2. + var len = ToLength(array.length); + + // Step 4. + if (len === 0) { + return ""; + } + + // Step 5. + var firstElement = array[0]; + + // Steps 6-7. + var R; + if (IsNullOrUndefined(firstElement)) { + R = ""; + } else { +#if JS_HAS_INTL_API + R = ToString( + callContentFunction( + firstElement.toLocaleString, + firstElement, + locales, + options + ) + ); +#else + R = ToString( + callContentFunction(firstElement.toLocaleString, firstElement) + ); +#endif + } + + // Step 3 (reordered). + // We don't (yet?) implement locale-dependent separators. + var separator = ","; + + // Steps 8-9. + for (var k = 1; k < len; k++) { + // Step 9.b. + var nextElement = array[k]; + + // Steps 9.a, 9.c-e. + R += separator; + if (!IsNullOrUndefined(nextElement)) { +#if JS_HAS_INTL_API + R += ToString( + callContentFunction( + nextElement.toLocaleString, + nextElement, + locales, + options + ) + ); +#else + R += ToString( + callContentFunction(nextElement.toLocaleString, nextElement) + ); +#endif + } + } + + // Step 10. + return R; +} + +// ES 2016 draft Mar 25, 2016 22.1.2.5. +function $ArraySpecies() { + // Step 1. + return this; +} +SetCanonicalName($ArraySpecies, "get [Symbol.species]"); + +// ES 2016 draft Mar 25, 2016 9.4.2.3. +function ArraySpeciesCreate(originalArray, length) { + // Step 1. + assert(typeof length === "number", "length should be a number"); + assert(length >= 0, "length should be a non-negative number"); + + // Step 2. + // eslint-disable-next-line no-compare-neg-zero + if (length === -0) { + length = 0; + } + + // Step 4, 6. + if (!IsArray(originalArray)) { + return std_Array(length); + } + + // Step 5.a. + var C = originalArray.constructor; + + // Step 5.b. + if (IsConstructor(C) && IsCrossRealmArrayConstructor(C)) { + return std_Array(length); + } + + // Step 5.c. + if (IsObject(C)) { + // Step 5.c.i. + C = C[GetBuiltinSymbol("species")]; + + // Optimized path for an ordinary Array. + if (C === GetBuiltinConstructor("Array")) { + return std_Array(length); + } + + // Step 5.c.ii. + if (C === null) { + return std_Array(length); + } + } + + // Step 6. + if (C === undefined) { + return std_Array(length); + } + + // Step 7. + if (!IsConstructor(C)) { + ThrowTypeError(JSMSG_NOT_CONSTRUCTOR, "constructor property"); + } + + // Step 8. + return constructContentFunction(C, C, length); +} + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 22.1.3.11 Array.prototype.flatMap ( mapperFunction [ , thisArg ] ) +function ArrayFlatMap(mapperFunction /*, thisArg*/) { + // Step 1. + var O = ToObject(this); + + // Step 2. + var sourceLen = ToLength(O.length); + + // Step 3. + if (!IsCallable(mapperFunction)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, mapperFunction)); + } + + // Step 4. + var T = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 5. + var A = ArraySpeciesCreate(O, 0); + + // Step 6. + FlattenIntoArray(A, O, sourceLen, 0, 1, mapperFunction, T); + + // Step 7. + return A; +} + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 22.1.3.10 Array.prototype.flat ( [ depth ] ) +function ArrayFlat(/* depth */) { + // Step 1. + var O = ToObject(this); + + // Step 2. + var sourceLen = ToLength(O.length); + + // Step 3. + var depthNum = 1; + + // Step 4. + if (ArgumentsLength() && GetArgument(0) !== undefined) { + depthNum = ToInteger(GetArgument(0)); + } + + // Step 5. + var A = ArraySpeciesCreate(O, 0); + + // Step 6. + FlattenIntoArray(A, O, sourceLen, 0, depthNum); + + // Step 7. + return A; +} + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 22.1.3.10.1 FlattenIntoArray ( target, source, sourceLen, start, depth [ , mapperFunction, thisArg ] ) +function FlattenIntoArray( + target, + source, + sourceLen, + start, + depth, + mapperFunction, + thisArg +) { + // Step 1. + var targetIndex = start; + + // Steps 2-3. + for (var sourceIndex = 0; sourceIndex < sourceLen; sourceIndex++) { + // Steps 3.a-c. + if (sourceIndex in source) { + // Step 3.c.i. + var element = source[sourceIndex]; + + if (mapperFunction) { + // Step 3.c.ii.1. + assert(ArgumentsLength() === 7, "thisArg is present"); + + // Step 3.c.ii.2. + element = callContentFunction( + mapperFunction, + thisArg, + element, + sourceIndex, + source + ); + } + + // Step 3.c.iii. + var shouldFlatten = false; + + // Step 3.c.iv. + if (depth > 0) { + // Step 3.c.iv.1. + shouldFlatten = IsArray(element); + } + + // Step 3.c.v. + if (shouldFlatten) { + // Step 3.c.v.1. + var elementLen = ToLength(element.length); + + // Step 3.c.v.2. + targetIndex = FlattenIntoArray( + target, + element, + elementLen, + targetIndex, + depth - 1 + ); + } else { + // Step 3.c.vi.1. + if (targetIndex >= MAX_NUMERIC_INDEX) { + ThrowTypeError(JSMSG_TOO_LONG_ARRAY); + } + + // Step 3.c.vi.2. + DefineDataProperty(target, targetIndex, element); + + // Step 3.c.vi.3. + targetIndex++; + } + } + } + + // Step 4. + return targetIndex; +} + +// https://github.com/tc39/proposal-relative-indexing-method +// Array.prototype.at ( index ) +function ArrayAt(index) { + // Step 1. + var O = ToObject(this); + + // Step 2. + var len = ToLength(O.length); + + // Step 3. + var relativeIndex = ToInteger(index); + + // Steps 4-5. + var k; + if (relativeIndex >= 0) { + k = relativeIndex; + } else { + k = len + relativeIndex; + } + + // Step 6. + if (k < 0 || k >= len) { + return undefined; + } + + // Step 7. + return O[k]; +} +// This function is only barely too long for normal inlining. +SetIsInlinableLargeFunction(ArrayAt); + +// https://github.com/tc39/proposal-change-array-by-copy +// Array.prototype.toReversed() +function ArrayToReversed() { + // Step 1. Let O be ? ToObject(this value). + var O = ToObject(this); + + // Step 2. Let len be ? LengthOfArrayLike(O). + var len = ToLength(O.length); + + // Step 3. Let A be ArrayCreate(𝔽(len)). + var A = std_Array(len); + + // Step 4. Let k be 0. + // Step 5. Repeat, while k < len, + for (var k = 0; k < len; k++) { + // Step 5.a. Let from be ! ToString(𝔽(len - k - 1)). + var from = len - k - 1; + + // Skip Step 5.b. Let Pk be ToString(𝔽(k)). + // k is coerced into a string through the property access. + + // Step 5.c. Let fromValue be ? Get(O, from). + var fromValue = O[from]; + + // Step 5.d. Perform ! CreateDataPropertyOrThrow(A, 𝔽(k), fromValue). + DefineDataProperty(A, k, fromValue); + } + + // Step 6. Return A. + return A; +} + +// https://github.com/tc39/proposal-change-array-by-copy +// Array.prototype.toSorted() +function ArrayToSorted(comparefn) { + // Step 1. If comparefn is not undefined and IsCallable(comparefn) is + // false, throw a TypeError exception. + if (comparefn !== undefined && !IsCallable(comparefn)) { + ThrowTypeError(JSMSG_BAD_TOSORTED_ARG); + } + + // Step 2. Let O be ? ToObject(this value). + var O = ToObject(this); + + // Step 3. Let len be ? LengthOfArrayLike(O). + var len = ToLength(O.length); + + // Step 4. Let A be ? ArrayCreate(𝔽(len)). + var items = std_Array(len); + + // We depart from steps 5-8 of the spec for performance reasons, as + // following the spec would require copying the input array twice. + // Instead, we create a new array that replaces holes with undefined, + // and sort this array. + for (var k = 0; k < len; k++) { + DefineDataProperty(items, k, O[k]); + } + + // Arrays with less than two elements remain unchanged when sorted. + if (len <= 1) { + return items; + } + + // First try to sort the array in native code, if that fails, indicated by + // returning |false| from ArrayNativeSort, sort it in self-hosted code. + if (callFunction(ArrayNativeSort, items, comparefn)) { + return items; + } + + // Step 5. + var wrappedCompareFn = ArraySortCompare(comparefn); + + // Steps 6-9. + var sorted = MergeSort(items, len, wrappedCompareFn); + + assert(IsPackedArray(sorted), "sorted is a packed array"); + assert(sorted.length === len, "sorted array has the correct length"); + + return sorted; +} + +// https://github.com/tc39/proposal-array-find-from-last +// Array.prototype.findLast ( predicate, thisArg ) +function ArrayFindLast(predicate /*, thisArg*/) { + // Step 1. + var O = ToObject(this); + + // Step 2. + var len = ToLength(O.length); + + // Step 3. + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Array.prototype.findLast"); + } + if (!IsCallable(predicate)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, predicate)); + } + + var thisArg = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Steps 4-5. + for (var k = len - 1; k >= 0; k--) { + // Steps 5.a-b. + var kValue = O[k]; + + // Steps 5.c-d. + if (callContentFunction(predicate, thisArg, kValue, k, O)) { + return kValue; + } + } + + // Step 6. + return undefined; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(ArrayFindLast); + +// https://github.com/tc39/proposal-array-find-from-last +// Array.prototype.findLastIndex ( predicate, thisArg ) +function ArrayFindLastIndex(predicate /*, thisArg*/) { + // Step 1. + var O = ToObject(this); + + // Steps 2. + var len = ToLength(O.length); + + // Step 3. + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Array.prototype.findLastIndex"); + } + if (!IsCallable(predicate)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, predicate)); + } + + var thisArg = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Steps 4-5. + for (var k = len - 1; k >= 0; k--) { + // Steps 5.a-d. + if (callContentFunction(predicate, thisArg, O[k], k, O)) { + return k; + } + } + + // Step 6. + return -1; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(ArrayFindLastIndex); diff --git a/js/src/builtin/AsyncFunction.js b/js/src/builtin/AsyncFunction.js new file mode 100644 index 0000000000..9ce2be8027 --- /dev/null +++ b/js/src/builtin/AsyncFunction.js @@ -0,0 +1,19 @@ +/* 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/. */ + +function AsyncFunctionNext(val) { + assert( + IsAsyncFunctionGeneratorObject(this), + "ThisArgument must be a generator object for async functions" + ); + return resumeGenerator(this, val, "next"); +} + +function AsyncFunctionThrow(val) { + assert( + IsAsyncFunctionGeneratorObject(this), + "ThisArgument must be a generator object for async functions" + ); + return resumeGenerator(this, val, "throw"); +} diff --git a/js/src/builtin/AsyncIteration.js b/js/src/builtin/AsyncIteration.js new file mode 100644 index 0000000000..a85048365d --- /dev/null +++ b/js/src/builtin/AsyncIteration.js @@ -0,0 +1,594 @@ +/* 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/. */ + +function AsyncIteratorIdentity() { + return this; +} + +function AsyncGeneratorNext(val) { + assert( + IsAsyncGeneratorObject(this), + "ThisArgument must be a generator object for async generators" + ); + return resumeGenerator(this, val, "next"); +} + +function AsyncGeneratorThrow(val) { + assert( + IsAsyncGeneratorObject(this), + "ThisArgument must be a generator object for async generators" + ); + return resumeGenerator(this, val, "throw"); +} + +function AsyncGeneratorReturn(val) { + assert( + IsAsyncGeneratorObject(this), + "ThisArgument must be a generator object for async generators" + ); + return resumeGenerator(this, val, "return"); +} + +/* ECMA262 7.4.7 AsyncIteratorClose */ +async function AsyncIteratorClose(iteratorRecord, value) { + // Step 3. + const iterator = iteratorRecord.iterator; + // Step 4. + const returnMethod = iterator.return; + // Step 5. + if (!IsNullOrUndefined(returnMethod)) { + const result = await callContentFunction(returnMethod, iterator); + // Step 8. + if (!IsObject(result)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, DecompileArg(0, result)); + } + } + // Step 5b & 9. + return value; +} + +/* Iterator Helpers proposal 1.1.1 */ +function GetAsyncIteratorDirectWrapper(obj) { + // Step 1. + if (!IsObject(obj)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, obj); + } + + // Step 2. + const nextMethod = obj.next; + // Step 3. + if (!IsCallable(nextMethod)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, nextMethod); + } + + // Steps 4-5. + return { + // Use a named function expression instead of a method definition, so + // we don't create an inferred name for this function at runtime. + [GetBuiltinSymbol("asyncIterator")]: function AsyncIteratorMethod() { + return this; + }, + next(value) { + return callContentFunction(nextMethod, obj, value); + }, + async return(value) { + const returnMethod = obj.return; + if (!IsNullOrUndefined(returnMethod)) { + return callContentFunction(returnMethod, obj, value); + } + return { done: true, value }; + }, + }; +} + +/* AsyncIteratorHelper object prototype methods. */ +function AsyncIteratorHelperNext(value) { + let O = this; + if (!IsObject(O) || (O = GuardToAsyncIteratorHelper(O)) === null) { + return callFunction( + CallAsyncIteratorHelperMethodIfWrapped, + this, + value, + "AsyncIteratorHelperNext" + ); + } + const generator = UnsafeGetReservedSlot( + O, + ASYNC_ITERATOR_HELPER_GENERATOR_SLOT + ); + return callFunction(IntrinsicAsyncGeneratorNext, generator, value); +} + +function AsyncIteratorHelperReturn(value) { + let O = this; + if (!IsObject(O) || (O = GuardToAsyncIteratorHelper(O)) === null) { + return callFunction( + CallAsyncIteratorHelperMethodIfWrapped, + this, + value, + "AsyncIteratorHelperReturn" + ); + } + const generator = UnsafeGetReservedSlot( + O, + ASYNC_ITERATOR_HELPER_GENERATOR_SLOT + ); + return callFunction(IntrinsicAsyncGeneratorReturn, generator, value); +} + +function AsyncIteratorHelperThrow(value) { + let O = this; + if (!IsObject(O) || (O = GuardToAsyncIteratorHelper(O)) === null) { + return callFunction( + CallAsyncIteratorHelperMethodIfWrapped, + this, + value, + "AsyncIteratorHelperThrow" + ); + } + const generator = UnsafeGetReservedSlot( + O, + ASYNC_ITERATOR_HELPER_GENERATOR_SLOT + ); + return callFunction(IntrinsicAsyncGeneratorThrow, generator, value); +} + +// AsyncIterator lazy Iterator Helper methods +// Iterator Helpers proposal 2.1.6.2-2.1.6.7 +// +// The AsyncIterator lazy methods are structured closely to how the Iterator +// lazy methods are. See builtin/Iterator.js for the reasoning. + +/* Iterator Helpers proposal 2.1.6.2 Prelude */ +function AsyncIteratorMap(mapper) { + // Step 1. + const iterated = GetIteratorDirect(this); + + // Step 2. + if (!IsCallable(mapper)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, mapper)); + } + + const iteratorHelper = NewAsyncIteratorHelper(); + const generator = AsyncIteratorMapGenerator(iterated, mapper); + callFunction(IntrinsicAsyncGeneratorNext, generator); + UnsafeSetReservedSlot( + iteratorHelper, + ASYNC_ITERATOR_HELPER_GENERATOR_SLOT, + generator + ); + return iteratorHelper; +} + +/* Iterator Helpers proposal 2.1.6.2 Body */ +async function* AsyncIteratorMapGenerator(iterated, mapper) { + // Step 1. + let lastValue; + // Step 2. + let needClose = true; + try { + yield; + needClose = false; + + for ( + let next = await IteratorNext(iterated, lastValue); + !next.done; + next = await IteratorNext(iterated, lastValue) + ) { + // Step c. + const value = next.value; + + // Steps d-i. + needClose = true; + lastValue = yield callContentFunction(mapper, undefined, value); + needClose = false; + } + } finally { + if (needClose) { + AsyncIteratorClose(iterated); + } + } +} + +/* Iterator Helpers proposal 2.1.6.3 Prelude */ +function AsyncIteratorFilter(filterer) { + // Step 1. + const iterated = GetIteratorDirect(this); + + // Step 2. + if (!IsCallable(filterer)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, filterer)); + } + + const iteratorHelper = NewAsyncIteratorHelper(); + const generator = AsyncIteratorFilterGenerator(iterated, filterer); + callFunction(IntrinsicAsyncGeneratorNext, generator); + UnsafeSetReservedSlot( + iteratorHelper, + ASYNC_ITERATOR_HELPER_GENERATOR_SLOT, + generator + ); + return iteratorHelper; +} + +/* Iterator Helpers proposal 2.1.6.3 Body */ +async function* AsyncIteratorFilterGenerator(iterated, filterer) { + // Step 1. + let lastValue; + // Step 2. + let needClose = true; + try { + yield; + needClose = false; + + for ( + let next = await IteratorNext(iterated, lastValue); + !next.done; + next = await IteratorNext(iterated, lastValue) + ) { + // Step c. + const value = next.value; + + // Steps d-h. + needClose = true; + if (await callContentFunction(filterer, undefined, value)) { + lastValue = yield value; + } + needClose = false; + } + } finally { + if (needClose) { + AsyncIteratorClose(iterated); + } + } +} + +/* Iterator Helpers proposal 2.1.6.4 Prelude */ +function AsyncIteratorTake(limit) { + // Step 1. + const iterated = GetIteratorDirect(this); + + // Step 2. + const remaining = ToInteger(limit); + // Step 3. + if (remaining < 0) { + ThrowRangeError(JSMSG_NEGATIVE_LIMIT); + } + + const iteratorHelper = NewAsyncIteratorHelper(); + const generator = AsyncIteratorTakeGenerator(iterated, remaining); + callFunction(IntrinsicAsyncGeneratorNext, generator); + UnsafeSetReservedSlot( + iteratorHelper, + ASYNC_ITERATOR_HELPER_GENERATOR_SLOT, + generator + ); + return iteratorHelper; +} + +/* Iterator Helpers proposal 2.1.6.4 Body */ +async function* AsyncIteratorTakeGenerator(iterated, remaining) { + // Step 1. + let lastValue; + // Step 2. + let needClose = true; + try { + yield; + needClose = false; + + for (; remaining > 0; remaining--) { + const next = await IteratorNext(iterated, lastValue); + if (next.done) { + return undefined; + } + + const value = next.value; + + needClose = true; + lastValue = yield value; + needClose = false; + } + } finally { + if (needClose) { + AsyncIteratorClose(iterated, undefined); + } + } + + return AsyncIteratorClose(iterated, undefined); +} + +/* Iterator Helpers proposal 2.1.6.5 Prelude */ +function AsyncIteratorDrop(limit) { + // Step 1. + const iterated = GetIteratorDirect(this); + + // Step 2. + const remaining = ToInteger(limit); + // Step 3. + if (remaining < 0) { + ThrowRangeError(JSMSG_NEGATIVE_LIMIT); + } + + const iteratorHelper = NewAsyncIteratorHelper(); + const generator = AsyncIteratorDropGenerator(iterated, remaining); + callFunction(IntrinsicAsyncGeneratorNext, generator); + UnsafeSetReservedSlot( + iteratorHelper, + ASYNC_ITERATOR_HELPER_GENERATOR_SLOT, + generator + ); + return iteratorHelper; +} + +/* Iterator Helpers proposal 2.1.6.5 Body */ +async function* AsyncIteratorDropGenerator(iterated, remaining) { + let needClose = true; + try { + yield; + needClose = false; + + // Step 1. + for (; remaining > 0; remaining--) { + const next = await IteratorNext(iterated); + if (next.done) { + return; + } + } + + // Step 2. + let lastValue; + // Step 3. + for ( + let next = await IteratorNext(iterated, lastValue); + !next.done; + next = await IteratorNext(iterated, lastValue) + ) { + // Steps c-d. + const value = next.value; + + needClose = true; + lastValue = yield value; + needClose = false; + } + } finally { + if (needClose) { + AsyncIteratorClose(iterated); + } + } +} + +/* Iterator Helpers proposal 2.1.6.6 Prelude */ +function AsyncIteratorAsIndexedPairs() { + // Step 1. + const iterated = GetIteratorDirect(this); + + const iteratorHelper = NewAsyncIteratorHelper(); + const generator = AsyncIteratorAsIndexedPairsGenerator(iterated); + callFunction(IntrinsicAsyncGeneratorNext, generator); + UnsafeSetReservedSlot( + iteratorHelper, + ASYNC_ITERATOR_HELPER_GENERATOR_SLOT, + generator + ); + return iteratorHelper; +} + +/* Iterator Helpers proposal 2.1.6.6 Body */ +async function* AsyncIteratorAsIndexedPairsGenerator(iterated) { + let needClose = true; + try { + yield; + needClose = false; + + // Step 2. + let lastValue; + // Step 3. + for ( + let next = await IteratorNext(iterated, lastValue), index = 0; + !next.done; + next = await IteratorNext(iterated, lastValue), index++ + ) { + // Steps c-g. + const value = next.value; + + needClose = true; + lastValue = yield [index, value]; + needClose = false; + } + } finally { + if (needClose) { + AsyncIteratorClose(iterated); + } + } +} + +/* Iterator Helpers proposal 2.1.6.7 Prelude */ +function AsyncIteratorFlatMap(mapper) { + // Step 1. + const iterated = GetIteratorDirect(this); + + // Step 2. + if (!IsCallable(mapper)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, mapper)); + } + + const iteratorHelper = NewAsyncIteratorHelper(); + const generator = AsyncIteratorFlatMapGenerator(iterated, mapper); + callFunction(IntrinsicAsyncGeneratorNext, generator); + UnsafeSetReservedSlot( + iteratorHelper, + ASYNC_ITERATOR_HELPER_GENERATOR_SLOT, + generator + ); + return iteratorHelper; +} + +/* Iterator Helpers proposal 2.1.6.7 Body */ +async function* AsyncIteratorFlatMapGenerator(iterated, mapper) { + let needClose = true; + try { + yield; + needClose = false; + + // Step 1. + for ( + let next = await IteratorNext(iterated); + !next.done; + next = await IteratorNext(iterated) + ) { + // Step c. + const value = next.value; + + needClose = true; + // Step d. + const mapped = await callContentFunction(mapper, undefined, value); + // Steps f-k. + for await (const innerValue of allowContentIter(mapped)) { + yield innerValue; + } + needClose = false; + } + } finally { + if (needClose) { + AsyncIteratorClose(iterated); + } + } +} + +/* Iterator Helpers proposal 2.1.6.8 */ +async function AsyncIteratorReduce(reducer /*, initialValue*/) { + // Step 1. + const iterated = GetAsyncIteratorDirectWrapper(this); + + // Step 2. + if (!IsCallable(reducer)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, reducer)); + } + + // Step 3. + let accumulator; + if (ArgumentsLength() === 1) { + // Step a. + const next = await callContentFunction(iterated.next, iterated); + if (!IsObject(next)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, DecompileArg(0, next)); + } + // Step b. + if (next.done) { + ThrowTypeError(JSMSG_EMPTY_ITERATOR_REDUCE); + } + // Step c. + accumulator = next.value; + } else { + // Step 4. + accumulator = GetArgument(1); + } + + // Step 5. + for await (const value of allowContentIter(iterated)) { + // Steps d-h. + accumulator = await callContentFunction( + reducer, + undefined, + accumulator, + value + ); + } + // Step 5b. + return accumulator; +} + +/* Iterator Helpers proposal 2.1.6.9 */ +async function AsyncIteratorToArray() { + // Step 1. + const iterated = { [GetBuiltinSymbol("asyncIterator")]: () => this }; + // Step 2. + const items = []; + let index = 0; + // Step 3. + for await (const value of allowContentIter(iterated)) { + // Step d. + DefineDataProperty(items, index++, value); + } + // Step 3b. + return items; +} + +/* Iterator Helpers proposal 2.1.6.10 */ +async function AsyncIteratorForEach(fn) { + // Step 1. + const iterated = GetAsyncIteratorDirectWrapper(this); + + // Step 2. + if (!IsCallable(fn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, fn)); + } + + // Step 3. + for await (const value of allowContentIter(iterated)) { + // Steps d-g. + await callContentFunction(fn, undefined, value); + } +} + +/* Iterator Helpers proposal 2.1.6.11 */ +async function AsyncIteratorSome(fn) { + // Step 1. + const iterated = GetAsyncIteratorDirectWrapper(this); + + // Step 2. + if (!IsCallable(fn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, fn)); + } + + // Step 3. + for await (const value of allowContentIter(iterated)) { + // Steps d-h. + if (await callContentFunction(fn, undefined, value)) { + return true; + } + } + // Step 3b. + return false; +} + +/* Iterator Helpers proposal 2.1.6.12 */ +async function AsyncIteratorEvery(fn) { + // Step 1. + const iterated = GetAsyncIteratorDirectWrapper(this); + + // Step 2. + if (!IsCallable(fn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, fn)); + } + + // Step 3. + for await (const value of allowContentIter(iterated)) { + // Steps d-h. + if (!(await callContentFunction(fn, undefined, value))) { + return false; + } + } + // Step 3b. + return true; +} + +/* Iterator Helpers proposal 2.1.6.13 */ +async function AsyncIteratorFind(fn) { + // Step 1. + const iterated = GetAsyncIteratorDirectWrapper(this); + + // Step 2. + if (!IsCallable(fn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, fn)); + } + + // Step 3. + for await (const value of allowContentIter(iterated)) { + // Steps d-h. + if (await callContentFunction(fn, undefined, value)) { + return value; + } + } +} diff --git a/js/src/builtin/AtomicsObject.cpp b/js/src/builtin/AtomicsObject.cpp new file mode 100644 index 0000000000..9b09544acd --- /dev/null +++ b/js/src/builtin/AtomicsObject.cpp @@ -0,0 +1,1078 @@ +/* -*- 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/. */ + +/* + * JS Atomics pseudo-module. + * + * See chapter 24.4 "The Atomics Object" and chapter 27 "Memory Model" in + * ECMAScript 2021 for the full specification. + */ + +#include "builtin/AtomicsObject.h" + +#include "mozilla/Atomics.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Maybe.h" +#include "mozilla/ScopeExit.h" + +#include "jsnum.h" + +#include "jit/AtomicOperations.h" +#include "jit/InlinableNatives.h" +#include "js/Class.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/PropertySpec.h" +#include "js/Result.h" +#include "js/WaitCallbacks.h" +#include "vm/GlobalObject.h" +#include "vm/TypedArrayObject.h" + +#include "vm/Compartment-inl.h" +#include "vm/JSObject-inl.h" + +using namespace js; + +static bool ReportBadArrayType(JSContext* cx) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_ATOMICS_BAD_ARRAY); + return false; +} + +static bool ReportDetachedArrayBuffer(JSContext* cx) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_DETACHED); + return false; +} + +static bool ReportOutOfRange(JSContext* cx) { + // Use JSMSG_BAD_INDEX here, it is what ToIndex uses for some cases that it + // reports directly. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_INDEX); + return false; +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// Plus: https://github.com/tc39/ecma262/pull/1908 +// 24.4.1.1 ValidateIntegerTypedArray ( typedArray [ , waitable ] ) +static bool ValidateIntegerTypedArray( + JSContext* cx, HandleValue typedArray, bool waitable, + MutableHandle<TypedArrayObject*> unwrappedTypedArray) { + // Step 1 (implicit). + + // Step 2. + auto* unwrapped = UnwrapAndTypeCheckValue<TypedArrayObject>( + cx, typedArray, [cx]() { ReportBadArrayType(cx); }); + if (!unwrapped) { + return false; + } + + if (unwrapped->hasDetachedBuffer()) { + return ReportDetachedArrayBuffer(cx); + } + + // Steps 3-6. + if (waitable) { + switch (unwrapped->type()) { + case Scalar::Int32: + case Scalar::BigInt64: + break; + default: + return ReportBadArrayType(cx); + } + } else { + switch (unwrapped->type()) { + case Scalar::Int8: + case Scalar::Uint8: + case Scalar::Int16: + case Scalar::Uint16: + case Scalar::Int32: + case Scalar::Uint32: + case Scalar::BigInt64: + case Scalar::BigUint64: + break; + default: + return ReportBadArrayType(cx); + } + } + + // Steps 7-9 (modified to return the TypedArray). + unwrappedTypedArray.set(unwrapped); + return true; +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.1.2 ValidateAtomicAccess ( typedArray, requestIndex ) +static bool ValidateAtomicAccess(JSContext* cx, + Handle<TypedArrayObject*> typedArray, + HandleValue requestIndex, size_t* index) { + // Step 1 (implicit). + + MOZ_ASSERT(!typedArray->hasDetachedBuffer()); + size_t length = typedArray->length(); + + // Step 2. + uint64_t accessIndex; + if (!ToIndex(cx, requestIndex, &accessIndex)) { + return false; + } + + // Steps 3-5. + if (accessIndex >= length) { + return ReportOutOfRange(cx); + } + + // Step 6. + *index = size_t(accessIndex); + return true; +} + +template <typename T> +struct ArrayOps { + using Type = T; + + static JS::Result<T> convertValue(JSContext* cx, HandleValue v) { + int32_t n; + if (!ToInt32(cx, v, &n)) { + return cx->alreadyReportedError(); + } + return static_cast<T>(n); + } + + static JS::Result<T> convertValue(JSContext* cx, HandleValue v, + MutableHandleValue result) { + double d; + if (!ToInteger(cx, v, &d)) { + return cx->alreadyReportedError(); + } + result.setNumber(d); + return static_cast<T>(JS::ToInt32(d)); + } + + static JS::Result<> storeResult(JSContext* cx, T v, + MutableHandleValue result) { + result.setInt32(v); + return Ok(); + } +}; + +template <> +JS::Result<> ArrayOps<uint32_t>::storeResult(JSContext* cx, uint32_t v, + MutableHandleValue result) { + // Always double typed so that the JITs can assume the types are stable. + result.setDouble(v); + return Ok(); +} + +template <> +struct ArrayOps<int64_t> { + using Type = int64_t; + + static JS::Result<int64_t> convertValue(JSContext* cx, HandleValue v) { + BigInt* bi = ToBigInt(cx, v); + if (!bi) { + return cx->alreadyReportedError(); + } + return BigInt::toInt64(bi); + } + + static JS::Result<int64_t> convertValue(JSContext* cx, HandleValue v, + MutableHandleValue result) { + BigInt* bi = ToBigInt(cx, v); + if (!bi) { + return cx->alreadyReportedError(); + } + result.setBigInt(bi); + return BigInt::toInt64(bi); + } + + static JS::Result<> storeResult(JSContext* cx, int64_t v, + MutableHandleValue result) { + BigInt* bi = BigInt::createFromInt64(cx, v); + if (!bi) { + return cx->alreadyReportedError(); + } + result.setBigInt(bi); + return Ok(); + } +}; + +template <> +struct ArrayOps<uint64_t> { + using Type = uint64_t; + + static JS::Result<uint64_t> convertValue(JSContext* cx, HandleValue v) { + BigInt* bi = ToBigInt(cx, v); + if (!bi) { + return cx->alreadyReportedError(); + } + return BigInt::toUint64(bi); + } + + static JS::Result<uint64_t> convertValue(JSContext* cx, HandleValue v, + MutableHandleValue result) { + BigInt* bi = ToBigInt(cx, v); + if (!bi) { + return cx->alreadyReportedError(); + } + result.setBigInt(bi); + return BigInt::toUint64(bi); + } + + static JS::Result<> storeResult(JSContext* cx, uint64_t v, + MutableHandleValue result) { + BigInt* bi = BigInt::createFromUint64(cx, v); + if (!bi) { + return cx->alreadyReportedError(); + } + result.setBigInt(bi); + return Ok(); + } +}; + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.1.11 AtomicReadModifyWrite ( typedArray, index, value, op ), steps 1-2. +// 24.4.1.12 AtomicLoad ( typedArray, index ), steps 1-2. +// 24.4.4 Atomics.compareExchange ( typedArray, index, ... ), steps 1-2. +// 24.4.9 Atomics.store ( typedArray, index, value ), steps 1-2. +template <typename Op> +bool AtomicAccess(JSContext* cx, HandleValue obj, HandleValue index, Op op) { + // Step 1. + Rooted<TypedArrayObject*> unwrappedTypedArray(cx); + if (!ValidateIntegerTypedArray(cx, obj, false, &unwrappedTypedArray)) { + return false; + } + + // Step 2. + size_t intIndex; + if (!ValidateAtomicAccess(cx, unwrappedTypedArray, index, &intIndex)) { + return false; + } + + switch (unwrappedTypedArray->type()) { + case Scalar::Int8: + return op(ArrayOps<int8_t>{}, unwrappedTypedArray, intIndex); + case Scalar::Uint8: + return op(ArrayOps<uint8_t>{}, unwrappedTypedArray, intIndex); + case Scalar::Int16: + return op(ArrayOps<int16_t>{}, unwrappedTypedArray, intIndex); + case Scalar::Uint16: + return op(ArrayOps<uint16_t>{}, unwrappedTypedArray, intIndex); + case Scalar::Int32: + return op(ArrayOps<int32_t>{}, unwrappedTypedArray, intIndex); + case Scalar::Uint32: + return op(ArrayOps<uint32_t>{}, unwrappedTypedArray, intIndex); + case Scalar::BigInt64: + return op(ArrayOps<int64_t>{}, unwrappedTypedArray, intIndex); + case Scalar::BigUint64: + return op(ArrayOps<uint64_t>{}, unwrappedTypedArray, intIndex); + case Scalar::Float32: + case Scalar::Float64: + case Scalar::Uint8Clamped: + case Scalar::MaxTypedArrayViewType: + case Scalar::Int64: + case Scalar::Simd128: + break; + } + MOZ_CRASH("Unsupported TypedArray type"); +} + +template <typename T> +static SharedMem<T*> TypedArrayData(JSContext* cx, TypedArrayObject* typedArray, + size_t index) { + if (typedArray->hasDetachedBuffer()) { + ReportDetachedArrayBuffer(cx); + return {}; + } + + SharedMem<void*> typedArrayData = typedArray->dataPointerEither(); + return typedArrayData.cast<T*>() + index; +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.4 Atomics.compareExchange ( typedArray, index, expectedValue, +// replacementValue ) +static bool atomics_compareExchange(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue typedArray = args.get(0); + HandleValue index = args.get(1); + + return AtomicAccess( + cx, typedArray, index, + [cx, &args](auto ops, Handle<TypedArrayObject*> unwrappedTypedArray, + size_t index) { + using T = typename decltype(ops)::Type; + + HandleValue expectedValue = args.get(2); + HandleValue replacementValue = args.get(3); + + T oldval; + JS_TRY_VAR_OR_RETURN_FALSE(cx, oldval, + ops.convertValue(cx, expectedValue)); + + T newval; + JS_TRY_VAR_OR_RETURN_FALSE(cx, newval, + ops.convertValue(cx, replacementValue)); + + SharedMem<T*> addr = TypedArrayData<T>(cx, unwrappedTypedArray, index); + if (!addr) { + return false; + } + + oldval = + jit::AtomicOperations::compareExchangeSeqCst(addr, oldval, newval); + + JS_TRY_OR_RETURN_FALSE(cx, ops.storeResult(cx, oldval, args.rval())); + return true; + }); +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.7 Atomics.load ( typedArray, index ) +static bool atomics_load(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue typedArray = args.get(0); + HandleValue index = args.get(1); + + return AtomicAccess( + cx, typedArray, index, + [cx, &args](auto ops, Handle<TypedArrayObject*> unwrappedTypedArray, + size_t index) { + using T = typename decltype(ops)::Type; + + SharedMem<T*> addr = TypedArrayData<T>(cx, unwrappedTypedArray, index); + if (!addr) { + return false; + } + + T v = jit::AtomicOperations::loadSeqCst(addr); + + JS_TRY_OR_RETURN_FALSE(cx, ops.storeResult(cx, v, args.rval())); + return true; + }); +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.9 Atomics.store ( typedArray, index, value ) +static bool atomics_store(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue typedArray = args.get(0); + HandleValue index = args.get(1); + + return AtomicAccess( + cx, typedArray, index, + [cx, &args](auto ops, Handle<TypedArrayObject*> unwrappedTypedArray, + size_t index) { + using T = typename decltype(ops)::Type; + + HandleValue value = args.get(2); + + T v; + JS_TRY_VAR_OR_RETURN_FALSE(cx, v, + ops.convertValue(cx, value, args.rval())); + + SharedMem<T*> addr = TypedArrayData<T>(cx, unwrappedTypedArray, index); + if (!addr) { + return false; + } + + jit::AtomicOperations::storeSeqCst(addr, v); + return true; + }); +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.1.11 AtomicReadModifyWrite ( typedArray, index, value, op ) +template <typename AtomicOp> +static bool AtomicReadModifyWrite(JSContext* cx, const CallArgs& args, + AtomicOp op) { + HandleValue typedArray = args.get(0); + HandleValue index = args.get(1); + + return AtomicAccess( + cx, typedArray, index, + [cx, &args, op](auto ops, Handle<TypedArrayObject*> unwrappedTypedArray, + size_t index) { + using T = typename decltype(ops)::Type; + + HandleValue value = args.get(2); + + T v; + JS_TRY_VAR_OR_RETURN_FALSE(cx, v, ops.convertValue(cx, value)); + + SharedMem<T*> addr = TypedArrayData<T>(cx, unwrappedTypedArray, index); + if (!addr) { + return false; + } + + v = op(addr, v); + + JS_TRY_OR_RETURN_FALSE(cx, ops.storeResult(cx, v, args.rval())); + return true; + }); +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.5 Atomics.exchange ( typedArray, index, value ) +static bool atomics_exchange(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + return AtomicReadModifyWrite(cx, args, [](auto addr, auto val) { + return jit::AtomicOperations::exchangeSeqCst(addr, val); + }); +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.2 Atomics.add ( typedArray, index, value ) +static bool atomics_add(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + return AtomicReadModifyWrite(cx, args, [](auto addr, auto val) { + return jit::AtomicOperations::fetchAddSeqCst(addr, val); + }); +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.10 Atomics.sub ( typedArray, index, value ) +static bool atomics_sub(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + return AtomicReadModifyWrite(cx, args, [](auto addr, auto val) { + return jit::AtomicOperations::fetchSubSeqCst(addr, val); + }); +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.3 Atomics.and ( typedArray, index, value ) +static bool atomics_and(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + return AtomicReadModifyWrite(cx, args, [](auto addr, auto val) { + return jit::AtomicOperations::fetchAndSeqCst(addr, val); + }); +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.8 Atomics.or ( typedArray, index, value ) +static bool atomics_or(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + return AtomicReadModifyWrite(cx, args, [](auto addr, auto val) { + return jit::AtomicOperations::fetchOrSeqCst(addr, val); + }); +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.13 Atomics.xor ( typedArray, index, value ) +static bool atomics_xor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + return AtomicReadModifyWrite(cx, args, [](auto addr, auto val) { + return jit::AtomicOperations::fetchXorSeqCst(addr, val); + }); +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.6 Atomics.isLockFree ( size ) +static bool atomics_isLockFree(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue v = args.get(0); + + // Step 1. + int32_t size; + if (v.isInt32()) { + size = v.toInt32(); + } else { + double dsize; + if (!ToInteger(cx, v, &dsize)) { + return false; + } + + // Step 7 (non-integer case only). + if (!mozilla::NumberEqualsInt32(dsize, &size)) { + args.rval().setBoolean(false); + return true; + } + } + + // Steps 2-7. + args.rval().setBoolean(jit::AtomicOperations::isLockfreeJS(size)); + return true; +} + +namespace js { + +// Represents one waiting worker. +// +// The type is declared opaque in SharedArrayObject.h. Instances of +// js::FutexWaiter are stack-allocated and linked onto a list across a +// call to FutexThread::wait(). +// +// The 'waiters' field of the SharedArrayRawBuffer points to the highest +// priority waiter in the list, and lower priority nodes are linked through +// the 'lower_pri' field. The 'back' field goes the other direction. +// The list is circular, so the 'lower_pri' field of the lowest priority +// node points to the first node in the list. The list has no dedicated +// header node. + +class FutexWaiter { + public: + FutexWaiter(size_t offset, JSContext* cx) + : offset(offset), cx(cx), lower_pri(nullptr), back(nullptr) {} + + size_t offset; // int32 element index within the SharedArrayBuffer + JSContext* cx; // The waiting thread + FutexWaiter* lower_pri; // Lower priority nodes in circular doubly-linked + // list of waiters + FutexWaiter* back; // Other direction +}; + +class AutoLockFutexAPI { + // We have to wrap this in a Maybe because of the way loading + // mozilla::Atomic pointers works. + mozilla::Maybe<js::UniqueLock<js::Mutex>> unique_; + + public: + AutoLockFutexAPI() { + js::Mutex* lock = FutexThread::lock_; + unique_.emplace(*lock); + } + + ~AutoLockFutexAPI() { unique_.reset(); } + + js::UniqueLock<js::Mutex>& unique() { return *unique_; } +}; + +} // namespace js + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.11 Atomics.wait ( typedArray, index, value, timeout ), steps 8-9, 14-25. +template <typename T> +static FutexThread::WaitResult AtomicsWait( + JSContext* cx, SharedArrayRawBuffer* sarb, size_t byteOffset, T value, + const mozilla::Maybe<mozilla::TimeDuration>& timeout) { + // Validation and other guards should ensure that this does not happen. + MOZ_ASSERT(sarb, "wait is only applicable to shared memory"); + + // Steps 8-9. + if (!cx->fx.canWait()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_ATOMICS_WAIT_NOT_ALLOWED); + return FutexThread::WaitResult::Error; + } + + SharedMem<T*> addr = + sarb->dataPointerShared().cast<T*>() + (byteOffset / sizeof(T)); + + // Steps 15 (reordered), 17.a and 23 (through destructor). + // This lock also protects the "waiters" field on SharedArrayRawBuffer, + // and it provides the necessary memory fence. + AutoLockFutexAPI lock; + + // Steps 16-17. + if (jit::AtomicOperations::loadSafeWhenRacy(addr) != value) { + return FutexThread::WaitResult::NotEqual; + } + + // Steps 14, 18-22. + FutexWaiter w(byteOffset, cx); + if (FutexWaiter* waiters = sarb->waiters()) { + w.lower_pri = waiters; + w.back = waiters->back; + waiters->back->lower_pri = &w; + waiters->back = &w; + } else { + w.lower_pri = w.back = &w; + sarb->setWaiters(&w); + } + + FutexThread::WaitResult retval = cx->fx.wait(cx, lock.unique(), timeout); + + if (w.lower_pri == &w) { + sarb->setWaiters(nullptr); + } else { + w.lower_pri->back = w.back; + w.back->lower_pri = w.lower_pri; + if (sarb->waiters() == &w) { + sarb->setWaiters(w.lower_pri); + } + } + + // Steps 24-25. + return retval; +} + +FutexThread::WaitResult js::atomics_wait_impl( + JSContext* cx, SharedArrayRawBuffer* sarb, size_t byteOffset, int32_t value, + const mozilla::Maybe<mozilla::TimeDuration>& timeout) { + return AtomicsWait(cx, sarb, byteOffset, value, timeout); +} + +FutexThread::WaitResult js::atomics_wait_impl( + JSContext* cx, SharedArrayRawBuffer* sarb, size_t byteOffset, int64_t value, + const mozilla::Maybe<mozilla::TimeDuration>& timeout) { + return AtomicsWait(cx, sarb, byteOffset, value, timeout); +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.11 Atomics.wait ( typedArray, index, value, timeout ), steps 6-25. +template <typename T> +static bool DoAtomicsWait(JSContext* cx, + Handle<TypedArrayObject*> unwrappedTypedArray, + size_t index, T value, HandleValue timeoutv, + MutableHandleValue r) { + mozilla::Maybe<mozilla::TimeDuration> timeout; + if (!timeoutv.isUndefined()) { + // Step 6. + double timeout_ms; + if (!ToNumber(cx, timeoutv, &timeout_ms)) { + return false; + } + + // Step 7. + if (!std::isnan(timeout_ms)) { + if (timeout_ms < 0) { + timeout = mozilla::Some(mozilla::TimeDuration::FromSeconds(0.0)); + } else if (!std::isinf(timeout_ms)) { + timeout = + mozilla::Some(mozilla::TimeDuration::FromMilliseconds(timeout_ms)); + } + } + } + + // Step 10. + Rooted<SharedArrayBufferObject*> unwrappedSab( + cx, unwrappedTypedArray->bufferShared()); + + // Step 11. + size_t offset = unwrappedTypedArray->byteOffset(); + + // Steps 12-13. + // The computation will not overflow because range checks have been + // performed. + size_t indexedPosition = index * sizeof(T) + offset; + + // Steps 8-9, 14-25. + switch (atomics_wait_impl(cx, unwrappedSab->rawBufferObject(), + indexedPosition, value, timeout)) { + case FutexThread::WaitResult::NotEqual: + r.setString(cx->names().futexNotEqual); + return true; + case FutexThread::WaitResult::OK: + r.setString(cx->names().futexOK); + return true; + case FutexThread::WaitResult::TimedOut: + r.setString(cx->names().futexTimedOut); + return true; + case FutexThread::WaitResult::Error: + return false; + default: + MOZ_CRASH("Should not happen"); + } +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.11 Atomics.wait ( typedArray, index, value, timeout ) +static bool atomics_wait(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue objv = args.get(0); + HandleValue index = args.get(1); + HandleValue valv = args.get(2); + HandleValue timeoutv = args.get(3); + MutableHandleValue r = args.rval(); + + // Step 1. + Rooted<TypedArrayObject*> unwrappedTypedArray(cx); + if (!ValidateIntegerTypedArray(cx, objv, true, &unwrappedTypedArray)) { + return false; + } + MOZ_ASSERT(unwrappedTypedArray->type() == Scalar::Int32 || + unwrappedTypedArray->type() == Scalar::BigInt64); + + // https://github.com/tc39/ecma262/pull/1908 + if (!unwrappedTypedArray->isSharedMemory()) { + return ReportBadArrayType(cx); + } + + // Step 2. + size_t intIndex; + if (!ValidateAtomicAccess(cx, unwrappedTypedArray, index, &intIndex)) { + return false; + } + + if (unwrappedTypedArray->type() == Scalar::Int32) { + // Step 5. + int32_t value; + if (!ToInt32(cx, valv, &value)) { + return false; + } + + // Steps 6-25. + return DoAtomicsWait(cx, unwrappedTypedArray, intIndex, value, timeoutv, r); + } + + MOZ_ASSERT(unwrappedTypedArray->type() == Scalar::BigInt64); + + // Step 4. + RootedBigInt value(cx, ToBigInt(cx, valv)); + if (!value) { + return false; + } + + // Steps 6-25. + return DoAtomicsWait(cx, unwrappedTypedArray, intIndex, + BigInt::toInt64(value), timeoutv, r); +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.12 Atomics.notify ( typedArray, index, count ), steps 10-16. +int64_t js::atomics_notify_impl(SharedArrayRawBuffer* sarb, size_t byteOffset, + int64_t count) { + // Validation should ensure this does not happen. + MOZ_ASSERT(sarb, "notify is only applicable to shared memory"); + + // Steps 12 (reordered), 15 (through destructor). + AutoLockFutexAPI lock; + + // Step 11 (reordered). + int64_t woken = 0; + + // Steps 10, 13-14. + FutexWaiter* waiters = sarb->waiters(); + if (waiters && count) { + FutexWaiter* iter = waiters; + do { + FutexWaiter* c = iter; + iter = iter->lower_pri; + if (c->offset != byteOffset || !c->cx->fx.isWaiting()) { + continue; + } + c->cx->fx.notify(FutexThread::NotifyExplicit); + // Overflow will be a problem only in two cases: + // (1) 128-bit systems with substantially more than 2^64 bytes of + // memory per process, and a very lightweight + // Atomics.waitAsync(). Obviously a future problem. + // (2) Bugs. + MOZ_RELEASE_ASSERT(woken < INT64_MAX); + ++woken; + if (count > 0) { + --count; + } + } while (count && iter != waiters); + } + + // Step 16. + return woken; +} + +// ES2021 draft rev bd868f20b8c574ad6689fba014b62a1dba819e56 +// 24.4.12 Atomics.notify ( typedArray, index, count ) +static bool atomics_notify(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue objv = args.get(0); + HandleValue index = args.get(1); + HandleValue countv = args.get(2); + MutableHandleValue r = args.rval(); + + // Step 1. + Rooted<TypedArrayObject*> unwrappedTypedArray(cx); + if (!ValidateIntegerTypedArray(cx, objv, true, &unwrappedTypedArray)) { + return false; + } + MOZ_ASSERT(unwrappedTypedArray->type() == Scalar::Int32 || + unwrappedTypedArray->type() == Scalar::BigInt64); + + // Step 2. + size_t intIndex; + if (!ValidateAtomicAccess(cx, unwrappedTypedArray, index, &intIndex)) { + return false; + } + + // Steps 3-4. + int64_t count; + if (countv.isUndefined()) { + count = -1; + } else { + double dcount; + if (!ToInteger(cx, countv, &dcount)) { + return false; + } + if (dcount < 0.0) { + dcount = 0.0; + } + count = dcount < double(1ULL << 63) ? int64_t(dcount) : -1; + } + + // https://github.com/tc39/ecma262/pull/1908 + if (!unwrappedTypedArray->isSharedMemory()) { + r.setInt32(0); + return true; + } + + // Step 5. + Rooted<SharedArrayBufferObject*> unwrappedSab( + cx, unwrappedTypedArray->bufferShared()); + + // Step 6. + size_t offset = unwrappedTypedArray->byteOffset(); + + // Steps 7-9. + // The computation will not overflow because range checks have been + // performed. + size_t elementSize = Scalar::byteSize(unwrappedTypedArray->type()); + size_t indexedPosition = intIndex * elementSize + offset; + + // Steps 10-16. + r.setNumber(double(atomics_notify_impl(unwrappedSab->rawBufferObject(), + indexedPosition, count))); + + return true; +} + +/* static */ +bool js::FutexThread::initialize() { + MOZ_ASSERT(!lock_); + lock_ = js_new<js::Mutex>(mutexid::FutexThread); + return lock_ != nullptr; +} + +/* static */ +void js::FutexThread::destroy() { + if (lock_) { + js::Mutex* lock = lock_; + js_delete(lock); + lock_ = nullptr; + } +} + +/* static */ +void js::FutexThread::lock() { + // Load the atomic pointer. + js::Mutex* lock = lock_; + + lock->lock(); +} + +/* static */ mozilla::Atomic<js::Mutex*, mozilla::SequentiallyConsistent> + FutexThread::lock_; + +/* static */ +void js::FutexThread::unlock() { + // Load the atomic pointer. + js::Mutex* lock = lock_; + + lock->unlock(); +} + +js::FutexThread::FutexThread() + : cond_(nullptr), state_(Idle), canWait_(false) {} + +bool js::FutexThread::initInstance() { + MOZ_ASSERT(lock_); + cond_ = js_new<js::ConditionVariable>(); + return cond_ != nullptr; +} + +void js::FutexThread::destroyInstance() { + if (cond_) { + js_delete(cond_); + } +} + +bool js::FutexThread::isWaiting() { + // When a worker is awoken for an interrupt it goes into state + // WaitingNotifiedForInterrupt for a short time before it actually + // wakes up and goes into WaitingInterrupted. In those states the + // worker is still waiting, and if an explicit notify arrives the + // worker transitions to Woken. See further comments in + // FutexThread::wait(). + return state_ == Waiting || state_ == WaitingInterrupted || + state_ == WaitingNotifiedForInterrupt; +} + +FutexThread::WaitResult js::FutexThread::wait( + JSContext* cx, js::UniqueLock<js::Mutex>& locked, + const mozilla::Maybe<mozilla::TimeDuration>& timeout) { + MOZ_ASSERT(&cx->fx == this); + MOZ_ASSERT(cx->fx.canWait()); + MOZ_ASSERT(state_ == Idle || state_ == WaitingInterrupted); + + // Disallow waiting when a runtime is processing an interrupt. + // See explanation below. + + if (state_ == WaitingInterrupted) { + UnlockGuard<Mutex> unlock(locked); + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_ATOMICS_WAIT_NOT_ALLOWED); + return WaitResult::Error; + } + + // Go back to Idle after returning. + auto onFinish = mozilla::MakeScopeExit([&] { state_ = Idle; }); + + const bool isTimed = timeout.isSome(); + + auto finalEnd = timeout.map([](const mozilla::TimeDuration& timeout) { + return mozilla::TimeStamp::Now() + timeout; + }); + + // 4000s is about the longest timeout slice that is guaranteed to + // work cross-platform. + auto maxSlice = mozilla::TimeDuration::FromSeconds(4000.0); + + for (;;) { + // If we are doing a timed wait, calculate the end time for this wait + // slice. + auto sliceEnd = finalEnd.map([&](mozilla::TimeStamp& finalEnd) { + auto sliceEnd = mozilla::TimeStamp::Now() + maxSlice; + if (finalEnd < sliceEnd) { + sliceEnd = finalEnd; + } + return sliceEnd; + }); + + state_ = Waiting; + + MOZ_ASSERT((cx->runtime()->beforeWaitCallback == nullptr) == + (cx->runtime()->afterWaitCallback == nullptr)); + mozilla::DebugOnly<bool> callbacksPresent = + cx->runtime()->beforeWaitCallback != nullptr; + + void* cookie = nullptr; + uint8_t clientMemory[JS::WAIT_CALLBACK_CLIENT_MAXMEM]; + if (cx->runtime()->beforeWaitCallback) { + cookie = (*cx->runtime()->beforeWaitCallback)(clientMemory); + } + + if (isTimed) { + (void)cond_->wait_until(locked, *sliceEnd); + } else { + cond_->wait(locked); + } + + MOZ_ASSERT((cx->runtime()->afterWaitCallback != nullptr) == + callbacksPresent); + if (cx->runtime()->afterWaitCallback) { + (*cx->runtime()->afterWaitCallback)(cookie); + } + + switch (state_) { + case FutexThread::Waiting: + // Timeout or spurious wakeup. + if (isTimed) { + auto now = mozilla::TimeStamp::Now(); + if (now >= *finalEnd) { + return WaitResult::TimedOut; + } + } + break; + + case FutexThread::Woken: + return WaitResult::OK; + + case FutexThread::WaitingNotifiedForInterrupt: + // The interrupt handler may reenter the engine. In that case + // there are two complications: + // + // - The waiting thread is not actually waiting on the + // condition variable so we have to record that it + // should be woken when the interrupt handler returns. + // To that end, we flag the thread as interrupted around + // the interrupt and check state_ when the interrupt + // handler returns. A notify() call that reaches the + // runtime during the interrupt sets state_ to Woken. + // + // - It is in principle possible for wait() to be + // reentered on the same thread/runtime and waiting on the + // same location and to yet again be interrupted and enter + // the interrupt handler. In this case, it is important + // that when another agent notifies waiters, all waiters using + // the same runtime on the same location are woken in LIFO + // order; FIFO may be the required order, but FIFO would + // fail to wake up the innermost call. Interrupts are + // outside any spec anyway. Also, several such suspended + // waiters may be woken at a time. + // + // For the time being we disallow waiting from within code + // that runs from within an interrupt handler; this may + // occasionally (very rarely) be surprising but is + // expedient. Other solutions exist, see bug #1131943. The + // code that performs the check is above, at the head of + // this function. + + state_ = WaitingInterrupted; + { + UnlockGuard<Mutex> unlock(locked); + if (!cx->handleInterrupt()) { + return WaitResult::Error; + } + } + if (state_ == Woken) { + return WaitResult::OK; + } + break; + + default: + MOZ_CRASH("Bad FutexState in wait()"); + } + } +} + +void js::FutexThread::notify(NotifyReason reason) { + MOZ_ASSERT(isWaiting()); + + if ((state_ == WaitingInterrupted || state_ == WaitingNotifiedForInterrupt) && + reason == NotifyExplicit) { + state_ = Woken; + return; + } + switch (reason) { + case NotifyExplicit: + state_ = Woken; + break; + case NotifyForJSInterrupt: + if (state_ == WaitingNotifiedForInterrupt) { + return; + } + state_ = WaitingNotifiedForInterrupt; + break; + default: + MOZ_CRASH("bad NotifyReason in FutexThread::notify()"); + } + cond_->notify_all(); +} + +const JSFunctionSpec AtomicsMethods[] = { + JS_INLINABLE_FN("compareExchange", atomics_compareExchange, 4, 0, + AtomicsCompareExchange), + JS_INLINABLE_FN("load", atomics_load, 2, 0, AtomicsLoad), + JS_INLINABLE_FN("store", atomics_store, 3, 0, AtomicsStore), + JS_INLINABLE_FN("exchange", atomics_exchange, 3, 0, AtomicsExchange), + JS_INLINABLE_FN("add", atomics_add, 3, 0, AtomicsAdd), + JS_INLINABLE_FN("sub", atomics_sub, 3, 0, AtomicsSub), + JS_INLINABLE_FN("and", atomics_and, 3, 0, AtomicsAnd), + JS_INLINABLE_FN("or", atomics_or, 3, 0, AtomicsOr), + JS_INLINABLE_FN("xor", atomics_xor, 3, 0, AtomicsXor), + JS_INLINABLE_FN("isLockFree", atomics_isLockFree, 1, 0, AtomicsIsLockFree), + JS_FN("wait", atomics_wait, 4, 0), + JS_FN("notify", atomics_notify, 3, 0), + JS_FN("wake", atomics_notify, 3, 0), // Legacy name + JS_FS_END}; + +static const JSPropertySpec AtomicsProperties[] = { + JS_STRING_SYM_PS(toStringTag, "Atomics", JSPROP_READONLY), JS_PS_END}; + +static JSObject* CreateAtomicsObject(JSContext* cx, JSProtoKey key) { + RootedObject proto(cx, &cx->global()->getObjectPrototype()); + return NewTenuredObjectWithGivenProto(cx, &AtomicsObject::class_, proto); +} + +static const ClassSpec AtomicsClassSpec = {CreateAtomicsObject, nullptr, + AtomicsMethods, AtomicsProperties}; + +const JSClass AtomicsObject::class_ = { + "Atomics", JSCLASS_HAS_CACHED_PROTO(JSProto_Atomics), JS_NULL_CLASS_OPS, + &AtomicsClassSpec}; diff --git a/js/src/builtin/AtomicsObject.h b/js/src/builtin/AtomicsObject.h new file mode 100644 index 0000000000..0f2e6c4af9 --- /dev/null +++ b/js/src/builtin/AtomicsObject.h @@ -0,0 +1,141 @@ +/* -*- 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 builtin_AtomicsObject_h +#define builtin_AtomicsObject_h + +#include "mozilla/Maybe.h" +#include "mozilla/TimeStamp.h" + +#include "threading/ConditionVariable.h" +#include "threading/ProtectedData.h" // js::ThreadData +#include "vm/NativeObject.h" + +namespace js { + +class SharedArrayRawBuffer; + +class AtomicsObject : public NativeObject { + public: + static const JSClass class_; +}; + +class FutexThread { + friend class AutoLockFutexAPI; + + public: + [[nodiscard]] static bool initialize(); + static void destroy(); + + static void lock(); + static void unlock(); + + FutexThread(); + [[nodiscard]] bool initInstance(); + void destroyInstance(); + + // Parameters to notify(). + enum NotifyReason { + NotifyExplicit, // Being asked to wake up by another thread + NotifyForJSInterrupt // Interrupt requested + }; + + // Result codes from wait() and atomics_wait_impl(). + enum class WaitResult { + Error, // Error has been reported, just propagate error signal + NotEqual, // Did not wait because the values differed + OK, // Waited and was woken + TimedOut // Waited and timed out + }; + + // Block the calling thread and wait. + // + // The futex lock must be held around this call. + // + // The timeout is the number of milliseconds, with fractional + // times allowed; specify mozilla::Nothing() for an indefinite + // wait. + // + // wait() will not wake up spuriously. + [[nodiscard]] WaitResult wait( + JSContext* cx, js::UniqueLock<js::Mutex>& locked, + const mozilla::Maybe<mozilla::TimeDuration>& timeout); + + // Notify the thread this is associated with. + // + // The futex lock must be held around this call. (The sleeping + // thread will not wake up until the caller of Atomics.notify() + // releases the lock.) + // + // If the thread is not waiting then this method does nothing. + // + // If the thread is waiting in a call to wait() and the + // reason is NotifyExplicit then the wait() call will return + // with Woken. + // + // If the thread is waiting in a call to wait() and the + // reason is NotifyForJSInterrupt then the wait() will return + // with WaitingNotifiedForInterrupt; in the latter case the caller + // of wait() must handle the interrupt. + void notify(NotifyReason reason); + + bool isWaiting(); + + // If canWait() returns false (the default) then wait() is disabled + // on the thread to which the FutexThread belongs. + bool canWait() { return canWait_; } + + void setCanWait(bool flag) { canWait_ = flag; } + + private: + enum FutexState { + Idle, // We are not waiting or woken + Waiting, // We are waiting, nothing has happened yet + WaitingNotifiedForInterrupt, // We are waiting, but have been interrupted, + // and have not yet started running the + // interrupt handler + WaitingInterrupted, // We are waiting, but have been interrupted + // and are running the interrupt handler + Woken // Woken by a script call to Atomics.notify + }; + + // Condition variable that this runtime will wait on. + js::ConditionVariable* cond_; + + // Current futex state for this runtime. When not in a wait this + // is Idle; when in a wait it is Waiting or the reason the futex + // is about to wake up. + FutexState state_; + + // Shared futex lock for all runtimes. We can perhaps do better, + // but any lock will need to be per-domain (consider SharedWorker) + // or coarser. + static mozilla::Atomic<js::Mutex*, mozilla::SequentiallyConsistent> lock_; + + // A flag that controls whether waiting is allowed. + ThreadData<bool> canWait_; +}; + +// Go to sleep if the int32_t value at the given address equals `value`. +[[nodiscard]] FutexThread::WaitResult atomics_wait_impl( + JSContext* cx, SharedArrayRawBuffer* sarb, size_t byteOffset, int32_t value, + const mozilla::Maybe<mozilla::TimeDuration>& timeout); + +// Go to sleep if the int64_t value at the given address equals `value`. +[[nodiscard]] FutexThread::WaitResult atomics_wait_impl( + JSContext* cx, SharedArrayRawBuffer* sarb, size_t byteOffset, int64_t value, + const mozilla::Maybe<mozilla::TimeDuration>& timeout); + +// Notify some waiters on the given address. If `count` is negative then notify +// all. The return value is nonnegative and is the number of waiters woken. If +// the number of waiters woken exceeds INT64_MAX then this never returns. If +// `count` is nonnegative then the return value is never greater than `count`. +[[nodiscard]] int64_t atomics_notify_impl(SharedArrayRawBuffer* sarb, + size_t byteOffset, int64_t count); + +} /* namespace js */ + +#endif /* builtin_AtomicsObject_h */ diff --git a/js/src/builtin/BigInt.cpp b/js/src/builtin/BigInt.cpp new file mode 100644 index 0000000000..378c383295 --- /dev/null +++ b/js/src/builtin/BigInt.cpp @@ -0,0 +1,234 @@ +/* -*- 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 "builtin/BigInt.h" + +#include "jit/InlinableNatives.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/PropertySpec.h" +#include "vm/BigIntType.h" + +#include "vm/GeckoProfiler-inl.h" +#include "vm/JSObject-inl.h" + +using namespace js; + +static MOZ_ALWAYS_INLINE bool IsBigInt(HandleValue v) { + return v.isBigInt() || (v.isObject() && v.toObject().is<BigIntObject>()); +} + +// BigInt proposal section 5.1.3 +static bool BigIntConstructor(JSContext* cx, unsigned argc, Value* vp) { + AutoJSConstructorProfilerEntry pseudoFrame(cx, "BigInt"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (args.isConstructing()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_CONSTRUCTOR, "BigInt"); + return false; + } + + // Step 2. + RootedValue v(cx, args.get(0)); + if (!ToPrimitive(cx, JSTYPE_NUMBER, &v)) { + return false; + } + + // Steps 3-4. + BigInt* bi = + v.isNumber() ? NumberToBigInt(cx, v.toNumber()) : ToBigInt(cx, v); + if (!bi) { + return false; + } + + args.rval().setBigInt(bi); + return true; +} + +JSObject* BigIntObject::create(JSContext* cx, HandleBigInt bigInt) { + BigIntObject* bn = NewBuiltinClassInstance<BigIntObject>(cx); + if (!bn) { + return nullptr; + } + bn->setFixedSlot(PRIMITIVE_VALUE_SLOT, BigIntValue(bigInt)); + return bn; +} + +BigInt* BigIntObject::unbox() const { + return getFixedSlot(PRIMITIVE_VALUE_SLOT).toBigInt(); +} + +// BigInt proposal section 5.3.4 +bool BigIntObject::valueOf_impl(JSContext* cx, const CallArgs& args) { + // Step 1. + HandleValue thisv = args.thisv(); + MOZ_ASSERT(IsBigInt(thisv)); + BigInt* bi = thisv.isBigInt() ? thisv.toBigInt() + : thisv.toObject().as<BigIntObject>().unbox(); + + args.rval().setBigInt(bi); + return true; +} + +bool BigIntObject::valueOf(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsBigInt, valueOf_impl>(cx, args); +} + +// BigInt proposal section 5.3.3 +bool BigIntObject::toString_impl(JSContext* cx, const CallArgs& args) { + // Step 1. + HandleValue thisv = args.thisv(); + MOZ_ASSERT(IsBigInt(thisv)); + RootedBigInt bi(cx, thisv.isBigInt() + ? thisv.toBigInt() + : thisv.toObject().as<BigIntObject>().unbox()); + + // Steps 2-3. + uint8_t radix = 10; + + // Steps 4-5. + if (args.hasDefined(0)) { + double d; + if (!ToInteger(cx, args[0], &d)) { + return false; + } + if (d < 2 || d > 36) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_RADIX); + return false; + } + radix = d; + } + + // Steps 6-7. + JSLinearString* str = BigInt::toString<CanGC>(cx, bi, radix); + if (!str) { + return false; + } + args.rval().setString(str); + return true; +} + +bool BigIntObject::toString(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "BigInt.prototype", "toString"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsBigInt, toString_impl>(cx, args); +} + +#ifndef JS_HAS_INTL_API +// BigInt proposal section 5.3.2. "This function is +// implementation-dependent, and it is permissible, but not encouraged, +// for it to return the same thing as toString." +bool BigIntObject::toLocaleString_impl(JSContext* cx, const CallArgs& args) { + HandleValue thisv = args.thisv(); + MOZ_ASSERT(IsBigInt(thisv)); + RootedBigInt bi(cx, thisv.isBigInt() + ? thisv.toBigInt() + : thisv.toObject().as<BigIntObject>().unbox()); + + JSString* str = BigInt::toString<CanGC>(cx, bi, 10); + if (!str) { + return false; + } + args.rval().setString(str); + return true; +} + +bool BigIntObject::toLocaleString(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "BigInt.prototype", + "toLocaleString"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsBigInt, toLocaleString_impl>(cx, args); +} +#endif /* !JS_HAS_INTL_API */ + +// BigInt proposal section 5.2.1. BigInt.asUintN ( bits, bigint ) +bool BigIntObject::asUintN(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + uint64_t bits; + if (!ToIndex(cx, args.get(0), &bits)) { + return false; + } + + // Step 2. + RootedBigInt bi(cx, ToBigInt(cx, args.get(1))); + if (!bi) { + return false; + } + + // Step 3. + BigInt* res = BigInt::asUintN(cx, bi, bits); + if (!res) { + return false; + } + + args.rval().setBigInt(res); + return true; +} + +// BigInt proposal section 5.2.2. BigInt.asIntN ( bits, bigint ) +bool BigIntObject::asIntN(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + uint64_t bits; + if (!ToIndex(cx, args.get(0), &bits)) { + return false; + } + + // Step 2. + RootedBigInt bi(cx, ToBigInt(cx, args.get(1))); + if (!bi) { + return false; + } + + // Step 3. + BigInt* res = BigInt::asIntN(cx, bi, bits); + if (!res) { + return false; + } + + args.rval().setBigInt(res); + return true; +} + +const ClassSpec BigIntObject::classSpec_ = { + GenericCreateConstructor<BigIntConstructor, 1, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<BigIntObject>, + BigIntObject::staticMethods, + nullptr, + BigIntObject::methods, + BigIntObject::properties}; + +const JSClass BigIntObject::class_ = { + "BigInt", + JSCLASS_HAS_CACHED_PROTO(JSProto_BigInt) | + JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS), + JS_NULL_CLASS_OPS, &BigIntObject::classSpec_}; + +const JSClass BigIntObject::protoClass_ = { + "BigInt.prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_BigInt), + JS_NULL_CLASS_OPS, &BigIntObject::classSpec_}; + +const JSPropertySpec BigIntObject::properties[] = { + // BigInt proposal section 5.3.5 + JS_STRING_SYM_PS(toStringTag, "BigInt", JSPROP_READONLY), JS_PS_END}; + +const JSFunctionSpec BigIntObject::methods[] = { + JS_FN("valueOf", valueOf, 0, 0), JS_FN("toString", toString, 0, 0), +#ifdef JS_HAS_INTL_API + JS_SELF_HOSTED_FN("toLocaleString", "BigInt_toLocaleString", 0, 0), +#else + JS_FN("toLocaleString", toLocaleString, 0, 0), +#endif + JS_FS_END}; + +const JSFunctionSpec BigIntObject::staticMethods[] = { + JS_INLINABLE_FN("asUintN", asUintN, 2, 0, BigIntAsUintN), + JS_INLINABLE_FN("asIntN", asIntN, 2, 0, BigIntAsIntN), JS_FS_END}; diff --git a/js/src/builtin/BigInt.h b/js/src/builtin/BigInt.h new file mode 100644 index 0000000000..8fb2d85dbf --- /dev/null +++ b/js/src/builtin/BigInt.h @@ -0,0 +1,53 @@ +/* -*- 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 builtin_BigInt_h +#define builtin_BigInt_h + +#include "js/Class.h" +#include "js/RootingAPI.h" +#include "vm/NativeObject.h" + +namespace js { + +class GlobalObject; + +class BigIntObject : public NativeObject { + static const unsigned PRIMITIVE_VALUE_SLOT = 0; + static const unsigned RESERVED_SLOTS = 1; + + public: + static const ClassSpec classSpec_; + static const JSClass class_; + static const JSClass protoClass_; + + static JSObject* create(JSContext* cx, JS::Handle<JS::BigInt*> bi); + + // Methods defined on BigInt.prototype. + static bool valueOf_impl(JSContext* cx, const CallArgs& args); + static bool valueOf(JSContext* cx, unsigned argc, JS::Value* vp); + static bool toString_impl(JSContext* cx, const CallArgs& args); + static bool toString(JSContext* cx, unsigned argc, JS::Value* vp); +#ifndef JS_HAS_INTL_API + static bool toLocaleString_impl(JSContext* cx, const CallArgs& args); + static bool toLocaleString(JSContext* cx, unsigned argc, JS::Value* vp); +#endif + static bool asUintN(JSContext* cx, unsigned argc, JS::Value* vp); + static bool asIntN(JSContext* cx, unsigned argc, JS::Value* vp); + + JS::BigInt* unbox() const; + + private: + static const JSPropertySpec properties[]; + static const JSFunctionSpec methods[]; + static const JSFunctionSpec staticMethods[]; +}; + +extern JSObject* InitBigIntClass(JSContext* cx, Handle<GlobalObject*> global); + +} // namespace js + +#endif diff --git a/js/src/builtin/BigInt.js b/js/src/builtin/BigInt.js new file mode 100644 index 0000000000..c7aa3859a8 --- /dev/null +++ b/js/src/builtin/BigInt.js @@ -0,0 +1,36 @@ +/* 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/. */ + +#if JS_HAS_INTL_API +/** + * Format this BigInt object into a string, using the locale and formatting + * options provided. + * + * Spec PR: https://github.com/tc39/ecma402/pull/236 + */ +function BigInt_toLocaleString() { + // Step 1. Note that valueOf enforces "thisBigIntValue" restrictions. + var x = callFunction(std_BigInt_valueOf, this); + + var locales = ArgumentsLength() ? GetArgument(0) : undefined; + var options = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 2. + var numberFormat; + if (locales === undefined && options === undefined) { + // This cache only optimizes when no explicit locales and options + // arguments were supplied. + if (!intl_IsRuntimeDefaultLocale(numberFormatCache.runtimeDefaultLocale)) { + numberFormatCache.numberFormat = intl_NumberFormat(locales, options); + numberFormatCache.runtimeDefaultLocale = intl_RuntimeDefaultLocale(); + } + numberFormat = numberFormatCache.numberFormat; + } else { + numberFormat = intl_NumberFormat(locales, options); + } + + // Step 3. + return intl_FormatNumber(numberFormat, x, /* formatToParts = */ false); +} +#endif // JS_HAS_INTL_API diff --git a/js/src/builtin/Boolean-inl.h b/js/src/builtin/Boolean-inl.h new file mode 100644 index 0000000000..e4c02e1bd2 --- /dev/null +++ b/js/src/builtin/Boolean-inl.h @@ -0,0 +1,29 @@ +/* -*- 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 builtin_Boolean_inl_h +#define builtin_Boolean_inl_h + +#include "builtin/Boolean.h" + +#include "vm/JSContext.h" +#include "vm/WrapperObject.h" + +namespace js { + +inline bool EmulatesUndefined(JSObject* obj) { + // This may be called off the main thread. It's OK not to expose the object + // here as it doesn't escape. + AutoUnsafeCallWithABI unsafe(UnsafeABIStrictness::AllowPendingExceptions); + JSObject* actual = MOZ_LIKELY(!obj->is<WrapperObject>()) + ? obj + : UncheckedUnwrapWithoutExpose(obj); + return actual->getClass()->emulatesUndefined(); +} + +} /* namespace js */ + +#endif /* builtin_Boolean_inl_h */ diff --git a/js/src/builtin/Boolean.cpp b/js/src/builtin/Boolean.cpp new file mode 100644 index 0000000000..a372efc9f0 --- /dev/null +++ b/js/src/builtin/Boolean.cpp @@ -0,0 +1,178 @@ +/* -*- 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/. */ + +/* + * JS boolean implementation. + */ + +#include "builtin/Boolean-inl.h" + +#include "jstypes.h" + +#include "jit/InlinableNatives.h" +#include "js/PropertySpec.h" +#include "util/StringBuffer.h" +#include "vm/BigIntType.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/BooleanObject-inl.h" + +using namespace js; + +const JSClass BooleanObject::class_ = { + "Boolean", + JSCLASS_HAS_RESERVED_SLOTS(1) | JSCLASS_HAS_CACHED_PROTO(JSProto_Boolean), + JS_NULL_CLASS_OPS, &BooleanObject::classSpec_}; + +MOZ_ALWAYS_INLINE bool IsBoolean(HandleValue v) { + return v.isBoolean() || (v.isObject() && v.toObject().is<BooleanObject>()); +} + +// ES2020 draft rev ecb4178012d6b4d9abc13fcbd45f5c6394b832ce +// 19.4.3 Properties of the Boolean Prototype Object, thisBooleanValue. +static MOZ_ALWAYS_INLINE bool ThisBooleanValue(HandleValue val) { + // Step 3, the error case, is handled by CallNonGenericMethod. + MOZ_ASSERT(IsBoolean(val)); + + // Step 1. + if (val.isBoolean()) { + return val.toBoolean(); + } + + // Step 2. + return val.toObject().as<BooleanObject>().unbox(); +} + +MOZ_ALWAYS_INLINE bool bool_toSource_impl(JSContext* cx, const CallArgs& args) { + bool b = ThisBooleanValue(args.thisv()); + + JSStringBuilder sb(cx); + if (!sb.append("(new Boolean(") || !BooleanToStringBuffer(b, sb) || + !sb.append("))")) { + return false; + } + + JSString* str = sb.finishString(); + if (!str) { + return false; + } + args.rval().setString(str); + return true; +} + +static bool bool_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsBoolean, bool_toSource_impl>(cx, args); +} + +// ES2020 draft rev ecb4178012d6b4d9abc13fcbd45f5c6394b832ce +// 19.3.3.2 Boolean.prototype.toString ( ) +MOZ_ALWAYS_INLINE bool bool_toString_impl(JSContext* cx, const CallArgs& args) { + // Step 1. + bool b = ThisBooleanValue(args.thisv()); + + // Step 2. + args.rval().setString(BooleanToString(cx, b)); + return true; +} + +static bool bool_toString(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsBoolean, bool_toString_impl>(cx, args); +} + +// ES2020 draft rev ecb4178012d6b4d9abc13fcbd45f5c6394b832ce +// 19.3.3.3 Boolean.prototype.valueOf ( ) +MOZ_ALWAYS_INLINE bool bool_valueOf_impl(JSContext* cx, const CallArgs& args) { + // Step 1. + args.rval().setBoolean(ThisBooleanValue(args.thisv())); + return true; +} + +static bool bool_valueOf(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsBoolean, bool_valueOf_impl>(cx, args); +} + +static const JSFunctionSpec boolean_methods[] = { + JS_FN(js_toSource_str, bool_toSource, 0, 0), + JS_FN(js_toString_str, bool_toString, 0, 0), + JS_FN(js_valueOf_str, bool_valueOf, 0, 0), JS_FS_END}; + +// ES2020 draft rev ecb4178012d6b4d9abc13fcbd45f5c6394b832ce +// 19.3.1.1 Boolean ( value ) +static bool Boolean(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + bool b = args.length() != 0 ? JS::ToBoolean(args[0]) : false; + + if (args.isConstructing()) { + // Steps 3-4. + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Boolean, + &proto)) { + return false; + } + + JSObject* obj = BooleanObject::create(cx, b, proto); + if (!obj) { + return false; + } + + // Step 5. + args.rval().setObject(*obj); + } else { + // Step 2. + args.rval().setBoolean(b); + } + return true; +} + +JSObject* BooleanObject::createPrototype(JSContext* cx, JSProtoKey key) { + BooleanObject* booleanProto = + GlobalObject::createBlankPrototype<BooleanObject>(cx, cx->global()); + if (!booleanProto) { + return nullptr; + } + booleanProto->setFixedSlot(BooleanObject::PRIMITIVE_VALUE_SLOT, + BooleanValue(false)); + return booleanProto; +} + +const ClassSpec BooleanObject::classSpec_ = { + GenericCreateConstructor<Boolean, 1, gc::AllocKind::FUNCTION, + &jit::JitInfo_Boolean>, + BooleanObject::createPrototype, + nullptr, + nullptr, + boolean_methods, + nullptr}; + +PropertyName* js::BooleanToString(JSContext* cx, bool b) { + return b ? cx->names().true_ : cx->names().false_; +} + +JS_PUBLIC_API bool js::ToBooleanSlow(HandleValue v) { + if (v.isString()) { + return v.toString()->length() != 0; + } + if (v.isBigInt()) { + return !v.toBigInt()->isZero(); + } +#ifdef ENABLE_RECORD_TUPLE + // proposal-record-tuple Section 3.1.1 + if (v.isExtendedPrimitive()) { + return true; + } +#endif + + MOZ_ASSERT(v.isObject()); + return !EmulatesUndefined(&v.toObject()); +} diff --git a/js/src/builtin/Boolean.h b/js/src/builtin/Boolean.h new file mode 100644 index 0000000000..c3723d2581 --- /dev/null +++ b/js/src/builtin/Boolean.h @@ -0,0 +1,23 @@ +/* -*- 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/. */ + +/* JS boolean interface. */ + +#ifndef builtin_Boolean_h +#define builtin_Boolean_h + +#include "jstypes.h" // JS_PUBLIC_API + +struct JS_PUBLIC_API JSContext; + +namespace js { +class PropertyName; + +extern PropertyName* BooleanToString(JSContext* cx, bool b); + +} // namespace js + +#endif /* builtin_Boolean_h */ diff --git a/js/src/builtin/DataViewObject.cpp b/js/src/builtin/DataViewObject.cpp new file mode 100644 index 0000000000..c02fc02c3c --- /dev/null +++ b/js/src/builtin/DataViewObject.cpp @@ -0,0 +1,1038 @@ +/* -*- 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 "builtin/DataViewObject.h" + +#include "mozilla/Casting.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/IntegerTypeTraits.h" +#include "mozilla/WrappingOperations.h" + +#include <algorithm> +#include <string.h> +#include <type_traits> + +#include "jsnum.h" + +#include "jit/AtomicOperations.h" +#include "jit/InlinableNatives.h" +#include "js/Conversions.h" +#include "js/experimental/TypedData.h" // JS_NewDataView +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/PropertySpec.h" +#include "js/Wrapper.h" +#include "util/DifferentialTesting.h" +#include "vm/ArrayBufferObject.h" +#include "vm/Compartment.h" +#include "vm/GlobalObject.h" +#include "vm/Interpreter.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/SharedMem.h" +#include "vm/Uint8Clamped.h" +#include "vm/WrapperObject.h" + +#include "vm/ArrayBufferObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +using JS::CanonicalizeNaN; +using JS::ToInt32; +using mozilla::AssertedCast; +using mozilla::WrapToSigned; + +DataViewObject* DataViewObject::create( + JSContext* cx, size_t byteOffset, size_t byteLength, + Handle<ArrayBufferObjectMaybeShared*> arrayBuffer, HandleObject proto) { + if (arrayBuffer->isDetached()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_DETACHED); + return nullptr; + } + + DataViewObject* obj = NewObjectWithClassProto<DataViewObject>(cx, proto); + if (!obj || !obj->init(cx, arrayBuffer, byteOffset, byteLength, + /* bytesPerElement = */ 1)) { + return nullptr; + } + + return obj; +} + +// ES2017 draft rev 931261ecef9b047b14daacf82884134da48dfe0f +// 24.3.2.1 DataView (extracted part of the main algorithm) +bool DataViewObject::getAndCheckConstructorArgs(JSContext* cx, + HandleObject bufobj, + const CallArgs& args, + size_t* byteOffsetPtr, + size_t* byteLengthPtr) { + // Step 3. + if (!bufobj->is<ArrayBufferObjectMaybeShared>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, "DataView", + "ArrayBuffer", bufobj->getClass()->name); + return false; + } + auto buffer = bufobj.as<ArrayBufferObjectMaybeShared>(); + + // Step 4. + uint64_t offset; + if (!ToIndex(cx, args.get(1), &offset)) { + return false; + } + + // Step 5. + if (buffer->isDetached()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + + // Step 6. + size_t bufferByteLength = buffer->byteLength(); + + // Step 7. + if (offset > bufferByteLength) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_OFFSET_OUT_OF_BUFFER); + return false; + } + MOZ_ASSERT(offset <= ArrayBufferObject::MaxByteLength); + + // Step 8.a + uint64_t viewByteLength = bufferByteLength - offset; + if (args.hasDefined(2)) { + // Step 9.a. + if (!ToIndex(cx, args.get(2), &viewByteLength)) { + return false; + } + + MOZ_ASSERT(offset + viewByteLength >= offset, + "can't overflow: both numbers are less than " + "DOUBLE_INTEGRAL_PRECISION_LIMIT"); + + // Step 9.b. + if (offset + viewByteLength > bufferByteLength) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_DATA_VIEW_LENGTH); + return false; + } + } + MOZ_ASSERT(viewByteLength <= ArrayBufferObject::MaxByteLength); + + *byteOffsetPtr = offset; + *byteLengthPtr = viewByteLength; + return true; +} + +bool DataViewObject::constructSameCompartment(JSContext* cx, + HandleObject bufobj, + const CallArgs& args) { + MOZ_ASSERT(args.isConstructing()); + cx->check(bufobj); + + size_t byteOffset = 0; + size_t byteLength = 0; + if (!getAndCheckConstructorArgs(cx, bufobj, args, &byteOffset, &byteLength)) { + return false; + } + + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_DataView, &proto)) { + return false; + } + + auto buffer = bufobj.as<ArrayBufferObjectMaybeShared>(); + JSObject* obj = + DataViewObject::create(cx, byteOffset, byteLength, buffer, proto); + if (!obj) { + return false; + } + args.rval().setObject(*obj); + return true; +} + +// Create a DataView object in another compartment. +// +// ES6 supports creating a DataView in global A (using global A's DataView +// constructor) backed by an ArrayBuffer created in global B. +// +// Our DataViewObject implementation doesn't support a DataView in +// compartment A backed by an ArrayBuffer in compartment B. So in this case, +// we create the DataView in B (!) and return a cross-compartment wrapper. +// +// Extra twist: the spec says the new DataView's [[Prototype]] must be +// A's DataView.prototype. So even though we're creating the DataView in B, +// its [[Prototype]] must be (a cross-compartment wrapper for) the +// DataView.prototype in A. +bool DataViewObject::constructWrapped(JSContext* cx, HandleObject bufobj, + const CallArgs& args) { + MOZ_ASSERT(args.isConstructing()); + MOZ_ASSERT(bufobj->is<WrapperObject>()); + + RootedObject unwrapped(cx, CheckedUnwrapStatic(bufobj)); + if (!unwrapped) { + ReportAccessDenied(cx); + return false; + } + + // NB: This entails the IsArrayBuffer check + size_t byteOffset = 0; + size_t byteLength = 0; + if (!getAndCheckConstructorArgs(cx, unwrapped, args, &byteOffset, + &byteLength)) { + return false; + } + + // Make sure to get the [[Prototype]] for the created view from this + // compartment. + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_DataView, &proto)) { + return false; + } + + Rooted<GlobalObject*> global(cx, cx->realm()->maybeGlobal()); + if (!proto) { + proto = GlobalObject::getOrCreateDataViewPrototype(cx, global); + if (!proto) { + return false; + } + } + + RootedObject dv(cx); + { + JSAutoRealm ar(cx, unwrapped); + + Rooted<ArrayBufferObjectMaybeShared*> buffer(cx); + buffer = &unwrapped->as<ArrayBufferObjectMaybeShared>(); + + RootedObject wrappedProto(cx, proto); + if (!cx->compartment()->wrap(cx, &wrappedProto)) { + return false; + } + + dv = DataViewObject::create(cx, byteOffset, byteLength, buffer, + wrappedProto); + if (!dv) { + return false; + } + } + + if (!cx->compartment()->wrap(cx, &dv)) { + return false; + } + + args.rval().setObject(*dv); + return true; +} + +bool DataViewObject::construct(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "DataView")) { + return false; + } + + RootedObject bufobj(cx); + if (!GetFirstArgumentAsObject(cx, args, "DataView constructor", &bufobj)) { + return false; + } + + if (bufobj->is<WrapperObject>()) { + return constructWrapped(cx, bufobj, args); + } + return constructSameCompartment(cx, bufobj, args); +} + +template <typename NativeType> +SharedMem<uint8_t*> DataViewObject::getDataPointer(uint64_t offset, + bool* isSharedMemory) { + MOZ_ASSERT(offsetIsInBounds<NativeType>(offset)); + + MOZ_ASSERT(offset < SIZE_MAX); + *isSharedMemory = this->isSharedMemory(); + return dataPointerEither().cast<uint8_t*>() + size_t(offset); +} + +template <typename T> +static inline std::enable_if_t<sizeof(T) != 1> SwapBytes(T* value, + bool isLittleEndian) { + if (isLittleEndian) { + mozilla::NativeEndian::swapToLittleEndianInPlace(value, 1); + } else { + mozilla::NativeEndian::swapToBigEndianInPlace(value, 1); + } +} + +template <typename T> +static inline std::enable_if_t<sizeof(T) == 1> SwapBytes(T* value, + bool isLittleEndian) { + // mozilla::NativeEndian doesn't support int8_t/uint8_t types. +} + +static inline void Memcpy(uint8_t* dest, uint8_t* src, size_t nbytes) { + memcpy(dest, src, nbytes); +} + +static inline void Memcpy(uint8_t* dest, SharedMem<uint8_t*> src, + size_t nbytes) { + jit::AtomicOperations::memcpySafeWhenRacy(dest, src, nbytes); +} + +static inline void Memcpy(SharedMem<uint8_t*> dest, uint8_t* src, + size_t nbytes) { + jit::AtomicOperations::memcpySafeWhenRacy(dest, src, nbytes); +} + +template <typename DataType, typename BufferPtrType> +struct DataViewIO { + using ReadWriteType = + typename mozilla::UnsignedStdintTypeForSize<sizeof(DataType)>::Type; + + static constexpr auto alignMask = + std::min<size_t>(alignof(void*), sizeof(DataType)) - 1; + + static void fromBuffer(DataType* dest, BufferPtrType unalignedBuffer, + bool isLittleEndian) { + MOZ_ASSERT((reinterpret_cast<uintptr_t>(dest) & alignMask) == 0); + Memcpy((uint8_t*)dest, unalignedBuffer, sizeof(ReadWriteType)); + ReadWriteType* rwDest = reinterpret_cast<ReadWriteType*>(dest); + SwapBytes(rwDest, isLittleEndian); + } + + static void toBuffer(BufferPtrType unalignedBuffer, const DataType* src, + bool isLittleEndian) { + MOZ_ASSERT((reinterpret_cast<uintptr_t>(src) & alignMask) == 0); + ReadWriteType temp = *reinterpret_cast<const ReadWriteType*>(src); + SwapBytes(&temp, isLittleEndian); + Memcpy(unalignedBuffer, (uint8_t*)&temp, sizeof(ReadWriteType)); + } +}; + +template <typename NativeType> +NativeType DataViewObject::read(uint64_t offset, bool isLittleEndian) { + bool isSharedMemory; + SharedMem<uint8_t*> data = + getDataPointer<NativeType>(offset, &isSharedMemory); + MOZ_ASSERT(data); + + NativeType val = 0; + if (isSharedMemory) { + DataViewIO<NativeType, SharedMem<uint8_t*>>::fromBuffer(&val, data, + isLittleEndian); + } else { + DataViewIO<NativeType, uint8_t*>::fromBuffer(&val, data.unwrapUnshared(), + isLittleEndian); + } + + return val; +} + +template uint32_t DataViewObject::read(uint64_t offset, bool isLittleEndian); + +// https://tc39.github.io/ecma262/#sec-getviewvalue +// GetViewValue ( view, requestIndex, isLittleEndian, type ) +template <typename NativeType> +/* static */ +bool DataViewObject::read(JSContext* cx, Handle<DataViewObject*> obj, + const CallArgs& args, NativeType* val) { + // Step 1. done by the caller + // Step 2. unnecessary assert + + // Step 3. + uint64_t getIndex; + if (!ToIndex(cx, args.get(0), &getIndex)) { + return false; + } + + // Step 4. + bool isLittleEndian = args.length() >= 2 && ToBoolean(args[1]); + + // Steps 5-6. + if (obj->hasDetachedBuffer()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + + // Steps 7-10. + if (!obj->offsetIsInBounds<NativeType>(getIndex)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_OFFSET_OUT_OF_DATAVIEW); + return false; + } + + // Steps 11-12. + *val = obj->read<NativeType>(getIndex, isLittleEndian); + return true; +} + +template <typename T> +static inline T WrappingConvert(int32_t value) { + if (std::is_unsigned_v<T>) { + return static_cast<T>(value); + } + + return WrapToSigned(static_cast<typename std::make_unsigned_t<T>>(value)); +} + +template <typename NativeType> +static inline bool WebIDLCast(JSContext* cx, HandleValue value, + NativeType* out) { + int32_t i; + if (!ToInt32(cx, value, &i)) { + return false; + } + + *out = WrappingConvert<NativeType>(i); + return true; +} + +template <> +inline bool WebIDLCast<int64_t>(JSContext* cx, HandleValue value, + int64_t* out) { + BigInt* bi = ToBigInt(cx, value); + if (!bi) { + return false; + } + *out = BigInt::toInt64(bi); + return true; +} + +template <> +inline bool WebIDLCast<uint64_t>(JSContext* cx, HandleValue value, + uint64_t* out) { + BigInt* bi = ToBigInt(cx, value); + if (!bi) { + return false; + } + *out = BigInt::toUint64(bi); + return true; +} + +template <> +inline bool WebIDLCast<float>(JSContext* cx, HandleValue value, float* out) { + double temp; + if (!ToNumber(cx, value, &temp)) { + return false; + } + *out = static_cast<float>(temp); + return true; +} + +template <> +inline bool WebIDLCast<double>(JSContext* cx, HandleValue value, double* out) { + return ToNumber(cx, value, out); +} + +// https://tc39.github.io/ecma262/#sec-setviewvalue +// SetViewValue ( view, requestIndex, isLittleEndian, type, value ) +template <typename NativeType> +/* static */ +bool DataViewObject::write(JSContext* cx, Handle<DataViewObject*> obj, + const CallArgs& args) { + // Step 1. done by the caller + // Step 2. unnecessary assert + + // Step 3. + uint64_t getIndex; + if (!ToIndex(cx, args.get(0), &getIndex)) { + return false; + } + + // Steps 4-5. Call ToBigInt(value) or ToNumber(value) depending on the type. + NativeType value; + if (!WebIDLCast(cx, args.get(1), &value)) { + return false; + } + + // See the comment in ElementSpecific::doubleToNative. + if (js::SupportDifferentialTesting() && TypeIsFloatingPoint<NativeType>()) { + value = JS::CanonicalizeNaN(value); + } + + // Step 6. + bool isLittleEndian = args.length() >= 3 && ToBoolean(args[2]); + + // Steps 7-8. + if (obj->hasDetachedBuffer()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + + // Steps 9-12. + if (!obj->offsetIsInBounds<NativeType>(getIndex)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_OFFSET_OUT_OF_DATAVIEW); + return false; + } + + // Steps 13-14. + bool isSharedMemory; + SharedMem<uint8_t*> data = + obj->getDataPointer<NativeType>(getIndex, &isSharedMemory); + MOZ_ASSERT(data); + + if (isSharedMemory) { + DataViewIO<NativeType, SharedMem<uint8_t*>>::toBuffer(data, &value, + isLittleEndian); + } else { + DataViewIO<NativeType, uint8_t*>::toBuffer(data.unwrapUnshared(), &value, + isLittleEndian); + } + return true; +} + +bool DataViewObject::getInt8Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + int8_t val; + if (!read(cx, thisView, args, &val)) { + return false; + } + args.rval().setInt32(val); + return true; +} + +bool DataViewObject::fun_getInt8(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, getInt8Impl>(cx, args); +} + +bool DataViewObject::getUint8Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + uint8_t val; + if (!read(cx, thisView, args, &val)) { + return false; + } + args.rval().setInt32(val); + return true; +} + +bool DataViewObject::fun_getUint8(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, getUint8Impl>(cx, args); +} + +bool DataViewObject::getInt16Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + int16_t val; + if (!read(cx, thisView, args, &val)) { + return false; + } + args.rval().setInt32(val); + return true; +} + +bool DataViewObject::fun_getInt16(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, getInt16Impl>(cx, args); +} + +bool DataViewObject::getUint16Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + uint16_t val; + if (!read(cx, thisView, args, &val)) { + return false; + } + args.rval().setInt32(val); + return true; +} + +bool DataViewObject::fun_getUint16(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, getUint16Impl>(cx, args); +} + +bool DataViewObject::getInt32Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + int32_t val; + if (!read(cx, thisView, args, &val)) { + return false; + } + args.rval().setInt32(val); + return true; +} + +bool DataViewObject::fun_getInt32(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, getInt32Impl>(cx, args); +} + +bool DataViewObject::getUint32Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + uint32_t val; + if (!read(cx, thisView, args, &val)) { + return false; + } + args.rval().setNumber(val); + return true; +} + +bool DataViewObject::fun_getUint32(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, getUint32Impl>(cx, args); +} + +// BigInt proposal 7.26 +// DataView.prototype.getBigInt64 ( byteOffset [ , littleEndian ] ) +bool DataViewObject::getBigInt64Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + int64_t val; + if (!read(cx, thisView, args, &val)) { + return false; + } + + BigInt* bi = BigInt::createFromInt64(cx, val); + if (!bi) { + return false; + } + args.rval().setBigInt(bi); + return true; +} + +bool DataViewObject::fun_getBigInt64(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, getBigInt64Impl>(cx, args); +} + +// BigInt proposal 7.27 +// DataView.prototype.getBigUint64 ( byteOffset [ , littleEndian ] ) +bool DataViewObject::getBigUint64Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + int64_t val; + if (!read(cx, thisView, args, &val)) { + return false; + } + + BigInt* bi = BigInt::createFromUint64(cx, val); + if (!bi) { + return false; + } + args.rval().setBigInt(bi); + return true; +} + +bool DataViewObject::fun_getBigUint64(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, getBigUint64Impl>(cx, args); +} + +bool DataViewObject::getFloat32Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + float val; + if (!read(cx, thisView, args, &val)) { + return false; + } + + args.rval().setDouble(CanonicalizeNaN(val)); + return true; +} + +bool DataViewObject::fun_getFloat32(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, getFloat32Impl>(cx, args); +} + +bool DataViewObject::getFloat64Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + double val; + if (!read(cx, thisView, args, &val)) { + return false; + } + + args.rval().setDouble(CanonicalizeNaN(val)); + return true; +} + +bool DataViewObject::fun_getFloat64(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, getFloat64Impl>(cx, args); +} + +bool DataViewObject::setInt8Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + if (!write<int8_t>(cx, thisView, args)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +bool DataViewObject::fun_setInt8(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, setInt8Impl>(cx, args); +} + +bool DataViewObject::setUint8Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + if (!write<uint8_t>(cx, thisView, args)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +bool DataViewObject::fun_setUint8(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, setUint8Impl>(cx, args); +} + +bool DataViewObject::setInt16Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + if (!write<int16_t>(cx, thisView, args)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +bool DataViewObject::fun_setInt16(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, setInt16Impl>(cx, args); +} + +bool DataViewObject::setUint16Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + if (!write<uint16_t>(cx, thisView, args)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +bool DataViewObject::fun_setUint16(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, setUint16Impl>(cx, args); +} + +bool DataViewObject::setInt32Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + if (!write<int32_t>(cx, thisView, args)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +bool DataViewObject::fun_setInt32(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, setInt32Impl>(cx, args); +} + +bool DataViewObject::setUint32Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + if (!write<uint32_t>(cx, thisView, args)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +bool DataViewObject::fun_setUint32(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, setUint32Impl>(cx, args); +} + +// BigInt proposal 7.28 +// DataView.prototype.setBigInt64 ( byteOffset, value [ , littleEndian ] ) +bool DataViewObject::setBigInt64Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + if (!write<int64_t>(cx, thisView, args)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +bool DataViewObject::fun_setBigInt64(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, setBigInt64Impl>(cx, args); +} + +// BigInt proposal 7.29 +// DataView.prototype.setBigUint64 ( byteOffset, value [ , littleEndian ] ) +bool DataViewObject::setBigUint64Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + if (!write<uint64_t>(cx, thisView, args)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +bool DataViewObject::fun_setBigUint64(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, setBigUint64Impl>(cx, args); +} + +bool DataViewObject::setFloat32Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + if (!write<float>(cx, thisView, args)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +bool DataViewObject::fun_setFloat32(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, setFloat32Impl>(cx, args); +} + +bool DataViewObject::setFloat64Impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + if (!write<double>(cx, thisView, args)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +bool DataViewObject::fun_setFloat64(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, setFloat64Impl>(cx, args); +} + +bool DataViewObject::bufferGetterImpl(JSContext* cx, const CallArgs& args) { + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + args.rval().set(thisView->bufferValue()); + return true; +} + +bool DataViewObject::bufferGetter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, bufferGetterImpl>(cx, args); +} + +bool DataViewObject::byteLengthGetterImpl(JSContext* cx, const CallArgs& args) { + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + // Step 6. + if (thisView->hasDetachedBuffer()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + + // Step 7. + args.rval().set(thisView->byteLengthValue()); + return true; +} + +bool DataViewObject::byteLengthGetter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, byteLengthGetterImpl>(cx, args); +} + +bool DataViewObject::byteOffsetGetterImpl(JSContext* cx, const CallArgs& args) { + Rooted<DataViewObject*> thisView( + cx, &args.thisv().toObject().as<DataViewObject>()); + + // Step 6. + if (thisView->hasDetachedBuffer()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TYPED_ARRAY_DETACHED); + return false; + } + + // Step 7. + args.rval().set(thisView->byteOffsetValue()); + return true; +} + +bool DataViewObject::byteOffsetGetter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, byteOffsetGetterImpl>(cx, args); +} + +static const JSClassOps DataViewObjectClassOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + ArrayBufferViewObject::trace, // trace +}; + +const ClassSpec DataViewObject::classSpec_ = { + GenericCreateConstructor<DataViewObject::construct, 1, + gc::AllocKind::FUNCTION>, + GenericCreatePrototype<DataViewObject>, + nullptr, + nullptr, + DataViewObject::methods, + DataViewObject::properties}; + +const JSClass DataViewObject::class_ = { + "DataView", + JSCLASS_HAS_RESERVED_SLOTS(DataViewObject::RESERVED_SLOTS) | + JSCLASS_HAS_CACHED_PROTO(JSProto_DataView), + &DataViewObjectClassOps, &DataViewObject::classSpec_}; + +const JSClass* const JS::DataView::ClassPtr = &DataViewObject::class_; + +const JSClass DataViewObject::protoClass_ = { + "DataView.prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_DataView), + JS_NULL_CLASS_OPS, &DataViewObject::classSpec_}; + +const JSFunctionSpec DataViewObject::methods[] = { + JS_INLINABLE_FN("getInt8", DataViewObject::fun_getInt8, 1, 0, + DataViewGetInt8), + JS_INLINABLE_FN("getUint8", DataViewObject::fun_getUint8, 1, 0, + DataViewGetUint8), + JS_INLINABLE_FN("getInt16", DataViewObject::fun_getInt16, 1, 0, + DataViewGetInt16), + JS_INLINABLE_FN("getUint16", DataViewObject::fun_getUint16, 1, 0, + DataViewGetUint16), + JS_INLINABLE_FN("getInt32", DataViewObject::fun_getInt32, 1, 0, + DataViewGetInt32), + JS_INLINABLE_FN("getUint32", DataViewObject::fun_getUint32, 1, 0, + DataViewGetUint32), + JS_INLINABLE_FN("getFloat32", DataViewObject::fun_getFloat32, 1, 0, + DataViewGetFloat32), + JS_INLINABLE_FN("getFloat64", DataViewObject::fun_getFloat64, 1, 0, + DataViewGetFloat64), + JS_INLINABLE_FN("getBigInt64", DataViewObject::fun_getBigInt64, 1, 0, + DataViewGetBigInt64), + JS_INLINABLE_FN("getBigUint64", DataViewObject::fun_getBigUint64, 1, 0, + DataViewGetBigUint64), + JS_INLINABLE_FN("setInt8", DataViewObject::fun_setInt8, 2, 0, + DataViewSetInt8), + JS_INLINABLE_FN("setUint8", DataViewObject::fun_setUint8, 2, 0, + DataViewSetUint8), + JS_INLINABLE_FN("setInt16", DataViewObject::fun_setInt16, 2, 0, + DataViewSetInt16), + JS_INLINABLE_FN("setUint16", DataViewObject::fun_setUint16, 2, 0, + DataViewSetUint16), + JS_INLINABLE_FN("setInt32", DataViewObject::fun_setInt32, 2, 0, + DataViewSetInt32), + JS_INLINABLE_FN("setUint32", DataViewObject::fun_setUint32, 2, 0, + DataViewSetUint32), + JS_INLINABLE_FN("setFloat32", DataViewObject::fun_setFloat32, 2, 0, + DataViewSetFloat32), + JS_INLINABLE_FN("setFloat64", DataViewObject::fun_setFloat64, 2, 0, + DataViewSetFloat64), + JS_INLINABLE_FN("setBigInt64", DataViewObject::fun_setBigInt64, 2, 0, + DataViewSetBigInt64), + JS_INLINABLE_FN("setBigUint64", DataViewObject::fun_setBigUint64, 2, 0, + DataViewSetBigUint64), + JS_FS_END}; + +const JSPropertySpec DataViewObject::properties[] = { + JS_PSG("buffer", DataViewObject::bufferGetter, 0), + JS_PSG("byteLength", DataViewObject::byteLengthGetter, 0), + JS_PSG("byteOffset", DataViewObject::byteOffsetGetter, 0), + JS_STRING_SYM_PS(toStringTag, "DataView", JSPROP_READONLY), JS_PS_END}; + +JS_PUBLIC_API JSObject* JS_NewDataView(JSContext* cx, HandleObject buffer, + size_t byteOffset, size_t byteLength) { + JSProtoKey key = JSProto_DataView; + RootedObject constructor(cx, GlobalObject::getOrCreateConstructor(cx, key)); + if (!constructor) { + return nullptr; + } + + FixedConstructArgs<3> cargs(cx); + + cargs[0].setObject(*buffer); + cargs[1].setNumber(byteOffset); + cargs[2].setNumber(byteLength); + + RootedValue fun(cx, ObjectValue(*constructor)); + RootedObject obj(cx); + if (!Construct(cx, fun, cargs, fun, &obj)) { + return nullptr; + } + return obj; +} diff --git a/js/src/builtin/DataViewObject.h b/js/src/builtin/DataViewObject.h new file mode 100644 index 0000000000..83a24cf29b --- /dev/null +++ b/js/src/builtin/DataViewObject.h @@ -0,0 +1,169 @@ +/* -*- 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_DataViewObject_h +#define vm_DataViewObject_h + +#include "mozilla/CheckedInt.h" + +#include "js/Class.h" +#include "vm/ArrayBufferViewObject.h" +#include "vm/JSObject.h" + +namespace js { + +class ArrayBufferObjectMaybeShared; + +// In the DataViewObject, the private slot contains a raw pointer into +// the buffer. The buffer may be shared memory and the raw pointer +// should not be exposed without sharedness information accompanying +// it. + +class DataViewObject : public ArrayBufferViewObject { + private: + static const ClassSpec classSpec_; + + static bool is(HandleValue v) { + return v.isObject() && v.toObject().hasClass(&class_); + } + + template <typename NativeType> + SharedMem<uint8_t*> getDataPointer(uint64_t offset, bool* isSharedMemory); + + static bool bufferGetterImpl(JSContext* cx, const CallArgs& args); + static bool bufferGetter(JSContext* cx, unsigned argc, Value* vp); + + static bool byteLengthGetterImpl(JSContext* cx, const CallArgs& args); + static bool byteLengthGetter(JSContext* cx, unsigned argc, Value* vp); + + static bool byteOffsetGetterImpl(JSContext* cx, const CallArgs& args); + static bool byteOffsetGetter(JSContext* cx, unsigned argc, Value* vp); + + static bool getAndCheckConstructorArgs(JSContext* cx, HandleObject bufobj, + const CallArgs& args, + size_t* byteOffset, + size_t* byteLength); + static bool constructSameCompartment(JSContext* cx, HandleObject bufobj, + const CallArgs& args); + static bool constructWrapped(JSContext* cx, HandleObject bufobj, + const CallArgs& args); + + static DataViewObject* create( + JSContext* cx, size_t byteOffset, size_t byteLength, + Handle<ArrayBufferObjectMaybeShared*> arrayBuffer, HandleObject proto); + + public: + static const JSClass class_; + static const JSClass protoClass_; + + size_t byteLength() const { + return size_t(getFixedSlot(LENGTH_SLOT).toPrivate()); + } + + Value byteLengthValue() const { + size_t len = byteLength(); + return NumberValue(len); + } + + template <typename NativeType> + bool offsetIsInBounds(uint64_t offset) const { + return offsetIsInBounds(sizeof(NativeType), offset); + } + bool offsetIsInBounds(uint32_t byteSize, uint64_t offset) const { + MOZ_ASSERT(byteSize <= 8); + mozilla::CheckedInt<uint64_t> endOffset(offset); + endOffset += byteSize; + return endOffset.isValid() && endOffset.value() <= byteLength(); + } + + static bool isOriginalByteOffsetGetter(Native native) { + return native == byteOffsetGetter; + } + + static bool isOriginalByteLengthGetter(Native native) { + return native == byteLengthGetter; + } + + static bool construct(JSContext* cx, unsigned argc, Value* vp); + + static bool getInt8Impl(JSContext* cx, const CallArgs& args); + static bool fun_getInt8(JSContext* cx, unsigned argc, Value* vp); + + static bool getUint8Impl(JSContext* cx, const CallArgs& args); + static bool fun_getUint8(JSContext* cx, unsigned argc, Value* vp); + + static bool getInt16Impl(JSContext* cx, const CallArgs& args); + static bool fun_getInt16(JSContext* cx, unsigned argc, Value* vp); + + static bool getUint16Impl(JSContext* cx, const CallArgs& args); + static bool fun_getUint16(JSContext* cx, unsigned argc, Value* vp); + + static bool getInt32Impl(JSContext* cx, const CallArgs& args); + static bool fun_getInt32(JSContext* cx, unsigned argc, Value* vp); + + static bool getUint32Impl(JSContext* cx, const CallArgs& args); + static bool fun_getUint32(JSContext* cx, unsigned argc, Value* vp); + + static bool getBigInt64Impl(JSContext* cx, const CallArgs& args); + static bool fun_getBigInt64(JSContext* cx, unsigned argc, Value* vp); + + static bool getBigUint64Impl(JSContext* cx, const CallArgs& args); + static bool fun_getBigUint64(JSContext* cx, unsigned argc, Value* vp); + + static bool getFloat32Impl(JSContext* cx, const CallArgs& args); + static bool fun_getFloat32(JSContext* cx, unsigned argc, Value* vp); + + static bool getFloat64Impl(JSContext* cx, const CallArgs& args); + static bool fun_getFloat64(JSContext* cx, unsigned argc, Value* vp); + + static bool setInt8Impl(JSContext* cx, const CallArgs& args); + static bool fun_setInt8(JSContext* cx, unsigned argc, Value* vp); + + static bool setUint8Impl(JSContext* cx, const CallArgs& args); + static bool fun_setUint8(JSContext* cx, unsigned argc, Value* vp); + + static bool setInt16Impl(JSContext* cx, const CallArgs& args); + static bool fun_setInt16(JSContext* cx, unsigned argc, Value* vp); + + static bool setUint16Impl(JSContext* cx, const CallArgs& args); + static bool fun_setUint16(JSContext* cx, unsigned argc, Value* vp); + + static bool setInt32Impl(JSContext* cx, const CallArgs& args); + static bool fun_setInt32(JSContext* cx, unsigned argc, Value* vp); + + static bool setUint32Impl(JSContext* cx, const CallArgs& args); + static bool fun_setUint32(JSContext* cx, unsigned argc, Value* vp); + + static bool setBigInt64Impl(JSContext* cx, const CallArgs& args); + static bool fun_setBigInt64(JSContext* cx, unsigned argc, Value* vp); + + static bool setBigUint64Impl(JSContext* cx, const CallArgs& args); + static bool fun_setBigUint64(JSContext* cx, unsigned argc, Value* vp); + + static bool setFloat32Impl(JSContext* cx, const CallArgs& args); + static bool fun_setFloat32(JSContext* cx, unsigned argc, Value* vp); + + static bool setFloat64Impl(JSContext* cx, const CallArgs& args); + static bool fun_setFloat64(JSContext* cx, unsigned argc, Value* vp); + + template <typename NativeType> + NativeType read(uint64_t offset, bool isLittleEndian); + + template <typename NativeType> + static bool read(JSContext* cx, Handle<DataViewObject*> obj, + const CallArgs& args, NativeType* val); + template <typename NativeType> + static bool write(JSContext* cx, Handle<DataViewObject*> obj, + const CallArgs& args); + + private: + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; +}; + +} // namespace js + +#endif /* vm_DataViewObject_h */ diff --git a/js/src/builtin/Date.js b/js/src/builtin/Date.js new file mode 100644 index 0000000000..6d5d8b7a17 --- /dev/null +++ b/js/src/builtin/Date.js @@ -0,0 +1,172 @@ +/* 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/. */ + +#if JS_HAS_INTL_API +// This cache, once primed, has these properties: +// +// runtimeDefaultLocale: +// Locale information provided by the embedding, guiding SpiderMonkey's +// selection of a default locale. See intl_RuntimeDefaultLocale(), whose +// value controls the value returned by DefaultLocale() that's what's +// *actually* used. +// icuDefaultTimeZone: +// Time zone information provided by ICU. See intl_defaultTimeZone(), +// whose value controls the value returned by DefaultTimeZone() that's +// what's *actually* used. +// formatters: +// A Record storing formatters consistent with the above +// runtimeDefaultLocale/localTZA values, for use with the appropriate +// ES6 toLocale*String Date method when called with its first two +// arguments having the value |undefined|. +// +// The "formatters" Record has (some subset of) these properties, as determined +// by all values of the first argument passed to |GetCachedFormat|: +// +// dateTimeFormat: for Date's toLocaleString operation +// dateFormat: for Date's toLocaleDateString operation +// timeFormat: for Date's toLocaleTimeString operation +// +// Using this cache, then, requires +// 1) verifying the current runtimeDefaultLocale/icuDefaultTimeZone are +// consistent with cached values, then +// 2) seeing if the desired formatter is cached and returning it if so, or else +// 3) create the desired formatter and store and return it. +var dateTimeFormatCache = new_Record(); + +/** + * Get a cached DateTimeFormat formatter object, created like so: + * + * var opts = ToDateTimeOptions(undefined, required, defaults); + * return new Intl.DateTimeFormat(undefined, opts); + * + * |format| must be a key from the "formatters" Record described above. + */ +function GetCachedFormat(format, required, defaults) { + assert( + format === "dateTimeFormat" || + format === "dateFormat" || + format === "timeFormat", + "unexpected format key: please update the comment by dateTimeFormatCache" + ); + + var formatters; + if ( + !intl_IsRuntimeDefaultLocale(dateTimeFormatCache.runtimeDefaultLocale) || + !intl_isDefaultTimeZone(dateTimeFormatCache.icuDefaultTimeZone) + ) { + formatters = dateTimeFormatCache.formatters = new_Record(); + dateTimeFormatCache.runtimeDefaultLocale = intl_RuntimeDefaultLocale(); + dateTimeFormatCache.icuDefaultTimeZone = intl_defaultTimeZone(); + } else { + formatters = dateTimeFormatCache.formatters; + } + + var fmt = formatters[format]; + if (fmt === undefined) { + var options = ToDateTimeOptions(undefined, required, defaults); + fmt = formatters[format] = intl_DateTimeFormat(undefined, options); + } + + return fmt; +} + +/** + * Format this Date object into a date and time string, using the locale and + * formatting options provided. + * + * Spec: ECMAScript Language Specification, 5.1 edition, 15.9.5.5. + * Spec: ECMAScript Internationalization API Specification, 13.3.1. + */ +function Date_toLocaleString() { + // Steps 1-2. + var x = callFunction(ThisTimeValue, this, DATE_METHOD_LOCALE_STRING); + if (Number_isNaN(x)) { + return "Invalid Date"; + } + + // Steps 3-4. + var locales = ArgumentsLength() ? GetArgument(0) : undefined; + var options = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 5-6. + var dateTimeFormat; + if (locales === undefined && options === undefined) { + // This cache only optimizes for the old ES5 toLocaleString without + // locales and options. + dateTimeFormat = GetCachedFormat("dateTimeFormat", "any", "all"); + } else { + options = ToDateTimeOptions(options, "any", "all"); + dateTimeFormat = intl_DateTimeFormat(locales, options); + } + + // Step 7. + return intl_FormatDateTime(dateTimeFormat, x, /* formatToParts = */ false); +} + +/** + * Format this Date object into a date string, using the locale and formatting + * options provided. + * + * Spec: ECMAScript Language Specification, 5.1 edition, 15.9.5.6. + * Spec: ECMAScript Internationalization API Specification, 13.3.2. + */ +function Date_toLocaleDateString() { + // Steps 1-2. + var x = callFunction(ThisTimeValue, this, DATE_METHOD_LOCALE_DATE_STRING); + if (Number_isNaN(x)) { + return "Invalid Date"; + } + + // Steps 3-4. + var locales = ArgumentsLength() ? GetArgument(0) : undefined; + var options = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 5-6. + var dateTimeFormat; + if (locales === undefined && options === undefined) { + // This cache only optimizes for the old ES5 toLocaleDateString without + // locales and options. + dateTimeFormat = GetCachedFormat("dateFormat", "date", "date"); + } else { + options = ToDateTimeOptions(options, "date", "date"); + dateTimeFormat = intl_DateTimeFormat(locales, options); + } + + // Step 7. + return intl_FormatDateTime(dateTimeFormat, x, /* formatToParts = */ false); +} + +/** + * Format this Date object into a time string, using the locale and formatting + * options provided. + * + * Spec: ECMAScript Language Specification, 5.1 edition, 15.9.5.7. + * Spec: ECMAScript Internationalization API Specification, 13.3.3. + */ +function Date_toLocaleTimeString() { + // Steps 1-2. + var x = callFunction(ThisTimeValue, this, DATE_METHOD_LOCALE_TIME_STRING); + if (Number_isNaN(x)) { + return "Invalid Date"; + } + + // Steps 3-4. + var locales = ArgumentsLength() ? GetArgument(0) : undefined; + var options = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 5-6. + var dateTimeFormat; + if (locales === undefined && options === undefined) { + // This cache only optimizes for the old ES5 toLocaleTimeString without + // locales and options. + dateTimeFormat = GetCachedFormat("timeFormat", "time", "time"); + } else { + options = ToDateTimeOptions(options, "time", "time"); + dateTimeFormat = intl_DateTimeFormat(locales, options); + } + + // Step 7. + return intl_FormatDateTime(dateTimeFormat, x, /* formatToParts = */ false); +} +#endif // JS_HAS_INTL_API diff --git a/js/src/builtin/Error.js b/js/src/builtin/Error.js new file mode 100644 index 0000000000..a8633a0a53 --- /dev/null +++ b/js/src/builtin/Error.js @@ -0,0 +1,37 @@ +/* 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/. */ + +/* ES6 20140718 draft 19.5.3.4. */ +function ErrorToString() { + /* Steps 1-2. */ + var obj = this; + if (!IsObject(obj)) { + ThrowTypeError(JSMSG_INCOMPATIBLE_PROTO, "Error", "toString", "value"); + } + + /* Steps 3-5. */ + var name = obj.name; + name = name === undefined ? "Error" : ToString(name); + + /* Steps 6-8. */ + var msg = obj.message; + msg = msg === undefined ? "" : ToString(msg); + + /* Step 9. */ + if (name === "") { + return msg; + } + + /* Step 10. */ + if (msg === "") { + return name; + } + + /* Step 11. */ + return name + ": " + msg; +} + +function ErrorToStringWithTrailingNewline() { + return FUN_APPLY(ErrorToString, this, []) + "\n"; +} diff --git a/js/src/builtin/Eval.cpp b/js/src/builtin/Eval.cpp new file mode 100644 index 0000000000..28c3317d41 --- /dev/null +++ b/js/src/builtin/Eval.cpp @@ -0,0 +1,547 @@ +/* -*- 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 "builtin/Eval.h" + +#include "mozilla/HashFunctions.h" +#include "mozilla/Range.h" + +#include "frontend/BytecodeCompilation.h" +#include "gc/HashUtil.h" +#include "js/CompilationAndEvaluation.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/friend/JSMEnvironment.h" // JS::NewJSMEnvironment, JS::ExecuteInJSMEnvironment, JS::GetJSMEnvironmentOfScriptedCaller, JS::IsJSMEnvironment +#include "js/friend/WindowProxy.h" // js::IsWindowProxy +#include "js/SourceText.h" +#include "js/StableStringChars.h" +#include "vm/EnvironmentObject.h" +#include "vm/FrameIter.h" +#include "vm/GlobalObject.h" +#include "vm/Interpreter.h" +#include "vm/JSContext.h" +#include "vm/JSONParser.h" + +#include "gc/Marking-inl.h" +#include "vm/EnvironmentObject-inl.h" +#include "vm/JSContext-inl.h" +#include "vm/Stack-inl.h" + +using namespace js; + +using mozilla::AddToHash; +using mozilla::HashString; +using mozilla::RangedPtr; + +using JS::AutoCheckCannotGC; +using JS::AutoStableStringChars; +using JS::CompileOptions; +using JS::SourceOwnership; +using JS::SourceText; + +// We should be able to assert this for *any* fp->environmentChain(). +static void AssertInnerizedEnvironmentChain(JSContext* cx, JSObject& env) { +#ifdef DEBUG + RootedObject obj(cx); + for (obj = &env; obj; obj = obj->enclosingEnvironment()) { + MOZ_ASSERT(!IsWindowProxy(obj)); + } +#endif +} + +static bool IsEvalCacheCandidate(JSScript* script) { + if (!script->isDirectEvalInFunction()) { + return false; + } + + // Make sure there are no inner objects (which may be used directly by script + // and clobbered) or inner functions (which may have wrong scope). + for (JS::GCCellPtr gcThing : script->gcthings()) { + if (gcThing.is<JSObject>()) { + return false; + } + } + + return true; +} + +/* static */ +HashNumber EvalCacheHashPolicy::hash(const EvalCacheLookup& l) { + HashNumber hash = HashStringChars(l.str); + return AddToHash(hash, l.callerScript.get(), l.pc); +} + +/* static */ +bool EvalCacheHashPolicy::match(const EvalCacheEntry& cacheEntry, + const EvalCacheLookup& l) { + MOZ_ASSERT(IsEvalCacheCandidate(cacheEntry.script)); + + return EqualStrings(cacheEntry.str, l.str) && + cacheEntry.callerScript == l.callerScript && cacheEntry.pc == l.pc; +} + +// Add the script to the eval cache when EvalKernel is finished +class EvalScriptGuard { + JSContext* cx_; + Rooted<JSScript*> script_; + + /* These fields are only valid if lookup_.str is non-nullptr. */ + EvalCacheLookup lookup_; + mozilla::Maybe<DependentAddPtr<EvalCache>> p_; + + Rooted<JSLinearString*> lookupStr_; + + public: + explicit EvalScriptGuard(JSContext* cx) + : cx_(cx), script_(cx), lookup_(cx), lookupStr_(cx) {} + + ~EvalScriptGuard() { + if (script_ && !cx_->isExceptionPending()) { + script_->cacheForEval(); + EvalCacheEntry cacheEntry = {lookupStr_, script_, lookup_.callerScript, + lookup_.pc}; + lookup_.str = lookupStr_; + if (lookup_.str && IsEvalCacheCandidate(script_)) { + // Ignore failure to add cache entry. + if (!p_->add(cx_, cx_->caches().evalCache, lookup_, cacheEntry)) { + cx_->recoverFromOutOfMemory(); + } + } + } + } + + void lookupInEvalCache(JSLinearString* str, JSScript* callerScript, + jsbytecode* pc) { + lookupStr_ = str; + lookup_.str = str; + lookup_.callerScript = callerScript; + lookup_.pc = pc; + p_.emplace(cx_, cx_->caches().evalCache, lookup_); + if (*p_) { + script_ = (*p_)->script; + p_->remove(cx_, cx_->caches().evalCache, lookup_); + } + } + + void setNewScript(JSScript* script) { + // JSScript::fullyInitFromStencil has already called js_CallNewScriptHook. + MOZ_ASSERT(!script_ && script); + script_ = script; + } + + bool foundScript() { return !!script_; } + + HandleScript script() { + MOZ_ASSERT(script_); + return script_; + } +}; + +enum class EvalJSONResult { Failure, Success, NotJSON }; + +template <typename CharT> +static bool EvalStringMightBeJSON(const mozilla::Range<const CharT> chars) { + // If the eval string starts with '(' or '[' and ends with ')' or ']', it + // may be JSON. Try the JSON parser first because it's much faster. If + // the eval string isn't JSON, JSON parsing will probably fail quickly, so + // little time will be lost. + size_t length = chars.length(); + if (length < 2) { + return false; + } + + // It used to be that strings in JavaScript forbid U+2028 LINE SEPARATOR + // and U+2029 PARAGRAPH SEPARATOR, so something like + // + // eval("['" + "\u2028" + "']"); + // + // i.e. an array containing a string with a line separator in it, *would* + // be JSON but *would not* be valid JavaScript. Handing such a string to + // the JSON parser would then fail to recognize a syntax error. As of + // <https://tc39.github.io/proposal-json-superset/> JavaScript strings may + // contain these two code points, so it's safe to JSON-parse eval strings + // that contain them. + + CharT first = chars[0], last = chars[length - 1]; + return (first == '[' && last == ']') || (first == '(' && last == ')'); +} + +template <typename CharT> +static EvalJSONResult ParseEvalStringAsJSON( + JSContext* cx, const mozilla::Range<const CharT> chars, + MutableHandleValue rval) { + size_t len = chars.length(); + MOZ_ASSERT((chars[0] == '(' && chars[len - 1] == ')') || + (chars[0] == '[' && chars[len - 1] == ']')); + + auto jsonChars = (chars[0] == '[') ? chars + : mozilla::Range<const CharT>( + chars.begin().get() + 1U, len - 2); + + Rooted<JSONParser<CharT>> parser( + cx, JSONParser<CharT>(cx, jsonChars, + JSONParser<CharT>::ParseType::AttemptForEval)); + if (!parser.parse(rval)) { + return EvalJSONResult::Failure; + } + + return rval.isUndefined() ? EvalJSONResult::NotJSON : EvalJSONResult::Success; +} + +static EvalJSONResult TryEvalJSON(JSContext* cx, JSLinearString* str, + MutableHandleValue rval) { + if (str->hasLatin1Chars()) { + AutoCheckCannotGC nogc; + if (!EvalStringMightBeJSON(str->latin1Range(nogc))) { + return EvalJSONResult::NotJSON; + } + } else { + AutoCheckCannotGC nogc; + if (!EvalStringMightBeJSON(str->twoByteRange(nogc))) { + return EvalJSONResult::NotJSON; + } + } + + AutoStableStringChars linearChars(cx); + if (!linearChars.init(cx, str)) { + return EvalJSONResult::Failure; + } + + return linearChars.isLatin1() + ? ParseEvalStringAsJSON(cx, linearChars.latin1Range(), rval) + : ParseEvalStringAsJSON(cx, linearChars.twoByteRange(), rval); +} + +enum EvalType { DIRECT_EVAL, INDIRECT_EVAL }; + +// 18.2.1.1 PerformEval +// +// Common code implementing direct and indirect eval. +// +// Evaluate v, if it is a string, in the context of the given calling +// frame, with the provided scope chain, with the semantics of either a direct +// or indirect eval (see ES5 10.4.2). If this is an indirect eval, env +// must be the global lexical environment. +// +// On success, store the completion value in call.rval and return true. +static bool EvalKernel(JSContext* cx, HandleValue v, EvalType evalType, + AbstractFramePtr caller, HandleObject env, + jsbytecode* pc, MutableHandleValue vp) { + MOZ_ASSERT((evalType == INDIRECT_EVAL) == !caller); + MOZ_ASSERT((evalType == INDIRECT_EVAL) == !pc); + MOZ_ASSERT_IF(evalType == INDIRECT_EVAL, IsGlobalLexicalEnvironment(env)); + AssertInnerizedEnvironmentChain(cx, *env); + + // Step 2. + if (!v.isString()) { + vp.set(v); + return true; + } + + // Steps 3-4. + RootedString str(cx, v.toString()); + if (!cx->isRuntimeCodeGenEnabled(JS::RuntimeCode::JS, str)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_CSP_BLOCKED_EVAL); + return false; + } + + // Step 5 ff. + + // Per ES5, indirect eval runs in the global scope. (eval is specified this + // way so that the compiler can make assumptions about what bindings may or + // may not exist in the current frame if it doesn't see 'eval'.) + MOZ_ASSERT_IF( + evalType != DIRECT_EVAL, + cx->global() == &env->as<GlobalLexicalEnvironmentObject>().global()); + + Rooted<JSLinearString*> linearStr(cx, str->ensureLinear(cx)); + if (!linearStr) { + return false; + } + + RootedScript callerScript(cx, caller ? caller.script() : nullptr); + EvalJSONResult ejr = TryEvalJSON(cx, linearStr, vp); + if (ejr != EvalJSONResult::NotJSON) { + return ejr == EvalJSONResult::Success; + } + + EvalScriptGuard esg(cx); + + if (evalType == DIRECT_EVAL && caller.isFunctionFrame()) { + esg.lookupInEvalCache(linearStr, callerScript, pc); + } + + if (!esg.foundScript()) { + RootedScript maybeScript(cx); + unsigned lineno; + const char* filename; + bool mutedErrors; + uint32_t pcOffset; + if (evalType == DIRECT_EVAL) { + DescribeScriptedCallerForDirectEval(cx, callerScript, pc, &filename, + &lineno, &pcOffset, &mutedErrors); + maybeScript = callerScript; + } else { + DescribeScriptedCallerForCompilation(cx, &maybeScript, &filename, &lineno, + &pcOffset, &mutedErrors); + } + + const char* introducerFilename = filename; + if (maybeScript && maybeScript->scriptSource()->introducerFilename()) { + introducerFilename = maybeScript->scriptSource()->introducerFilename(); + } + + Rooted<Scope*> enclosing(cx); + if (evalType == DIRECT_EVAL) { + enclosing = callerScript->innermostScope(pc); + } else { + enclosing = &cx->global()->emptyGlobalScope(); + } + + CompileOptions options(cx); + options.setIsRunOnce(true) + .setNoScriptRval(false) + .setMutedErrors(mutedErrors) + .setDeferDebugMetadata(); + + RootedScript introScript(cx); + + if (evalType == DIRECT_EVAL && IsStrictEvalPC(pc)) { + options.setForceStrictMode(); + } + + if (introducerFilename) { + options.setFileAndLine(filename, 1); + options.setIntroductionInfo(introducerFilename, "eval", lineno, pcOffset); + introScript = maybeScript; + } else { + options.setFileAndLine("eval", 1); + options.setIntroductionType("eval"); + } + options.setNonSyntacticScope( + enclosing->hasOnChain(ScopeKind::NonSyntactic)); + + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, linearStr)) { + return false; + } + + SourceText<char16_t> srcBuf; + if (!srcBuf.initMaybeBorrowed(cx, linearChars)) { + return false; + } + + RootedScript script( + cx, frontend::CompileEvalScript(cx, options, srcBuf, enclosing, env)); + if (!script) { + return false; + } + + RootedValue undefValue(cx); + JS::InstantiateOptions instantiateOptions(options); + if (!JS::UpdateDebugMetadata(cx, script, instantiateOptions, undefValue, + nullptr, introScript, maybeScript)) { + return false; + } + + esg.setNewScript(script); + } + + return ExecuteKernel(cx, esg.script(), env, NullFramePtr() /* evalInFrame */, + vp); +} + +bool js::IndirectEval(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedObject globalLexical(cx, &cx->global()->lexicalEnvironment()); + + // Note we'll just pass |undefined| here, then return it directly (or throw + // if runtime codegen is disabled), if no argument is provided. + return EvalKernel(cx, args.get(0), INDIRECT_EVAL, NullFramePtr(), + globalLexical, nullptr, args.rval()); +} + +bool js::DirectEval(JSContext* cx, HandleValue v, MutableHandleValue vp) { + // Direct eval can assume it was called from an interpreted or baseline frame. + ScriptFrameIter iter(cx); + AbstractFramePtr caller = iter.abstractFramePtr(); + + MOZ_ASSERT(JSOp(*iter.pc()) == JSOp::Eval || + JSOp(*iter.pc()) == JSOp::StrictEval || + JSOp(*iter.pc()) == JSOp::SpreadEval || + JSOp(*iter.pc()) == JSOp::StrictSpreadEval); + MOZ_ASSERT(caller.realm() == caller.script()->realm()); + + RootedObject envChain(cx, caller.environmentChain()); + return EvalKernel(cx, v, DIRECT_EVAL, caller, envChain, iter.pc(), vp); +} + +bool js::IsAnyBuiltinEval(JSFunction* fun) { + return fun->maybeNative() == IndirectEval; +} + +static bool ExecuteInExtensibleLexicalEnvironment( + JSContext* cx, HandleScript scriptArg, + Handle<ExtensibleLexicalEnvironmentObject*> env) { + CHECK_THREAD(cx); + cx->check(env); + cx->check(scriptArg); + MOZ_RELEASE_ASSERT(scriptArg->hasNonSyntacticScope()); + + RootedValue rval(cx); + return ExecuteKernel(cx, scriptArg, env, NullFramePtr() /* evalInFrame */, + &rval); +} + +JS_PUBLIC_API bool js::ExecuteInFrameScriptEnvironment( + JSContext* cx, HandleObject objArg, HandleScript scriptArg, + MutableHandleObject envArg) { + RootedObject varEnv(cx, NonSyntacticVariablesObject::create(cx)); + if (!varEnv) { + return false; + } + + RootedObjectVector envChain(cx); + if (!envChain.append(objArg)) { + return false; + } + + RootedObject env(cx); + if (!js::CreateObjectsForEnvironmentChain(cx, envChain, varEnv, &env)) { + return false; + } + + // Create lexical environment with |this| == objArg, which should be a Gecko + // MessageManager. + // NOTE: This is required behavior for Gecko FrameScriptLoader, where some + // callers try to bind methods from the message manager in their scope chain + // to |this|, and will fail if it is not bound to a message manager. + ObjectRealm& realm = ObjectRealm::get(varEnv); + Rooted<NonSyntacticLexicalEnvironmentObject*> lexicalEnv( + cx, + realm.getOrCreateNonSyntacticLexicalEnvironment(cx, env, varEnv, objArg)); + if (!lexicalEnv) { + return false; + } + + if (!ExecuteInExtensibleLexicalEnvironment(cx, scriptArg, lexicalEnv)) { + return false; + } + + envArg.set(lexicalEnv); + return true; +} + +JS_PUBLIC_API JSObject* JS::NewJSMEnvironment(JSContext* cx) { + RootedObject varEnv(cx, NonSyntacticVariablesObject::create(cx)); + if (!varEnv) { + return nullptr; + } + + // Force the NonSyntacticLexicalEnvironmentObject to be created. + ObjectRealm& realm = ObjectRealm::get(varEnv); + MOZ_ASSERT(!realm.getNonSyntacticLexicalEnvironment(varEnv)); + if (!realm.getOrCreateNonSyntacticLexicalEnvironment(cx, varEnv)) { + return nullptr; + } + + return varEnv; +} + +JS_PUBLIC_API bool JS::ExecuteInJSMEnvironment(JSContext* cx, + HandleScript scriptArg, + HandleObject varEnv) { + RootedObjectVector emptyChain(cx); + return ExecuteInJSMEnvironment(cx, scriptArg, varEnv, emptyChain); +} + +JS_PUBLIC_API bool JS::ExecuteInJSMEnvironment(JSContext* cx, + HandleScript scriptArg, + HandleObject varEnv, + HandleObjectVector targetObj) { + cx->check(varEnv); + MOZ_ASSERT( + ObjectRealm::get(varEnv).getNonSyntacticLexicalEnvironment(varEnv)); + MOZ_DIAGNOSTIC_ASSERT(scriptArg->noScriptRval()); + + Rooted<ExtensibleLexicalEnvironmentObject*> env( + cx, ExtensibleLexicalEnvironmentObject::forVarEnvironment(varEnv)); + + // If the Gecko subscript loader specifies target objects, we need to add + // them to the environment. These are added after the NSVO environment. + if (!targetObj.empty()) { + // The environment chain will be as follows: + // GlobalObject / BackstagePass + // GlobalLexicalEnvironmentObject[this=global] + // NonSyntacticVariablesObject (the JSMEnvironment) + // NonSyntacticLexicalEnvironmentObject[this=nsvo] + // WithEnvironmentObject[target=targetObj] + // NonSyntacticLexicalEnvironmentObject[this=targetObj] (*) + // + // (*) This environment intercepts JSOp::GlobalThis. + + // Wrap the target objects in WithEnvironments. + RootedObject envChain(cx); + if (!js::CreateObjectsForEnvironmentChain(cx, targetObj, env, &envChain)) { + return false; + } + + // See CreateNonSyntacticEnvironmentChain + if (!JSObject::setQualifiedVarObj(cx, envChain)) { + return false; + } + + // Create an extensible lexical environment for the target object. + env = ObjectRealm::get(envChain).getOrCreateNonSyntacticLexicalEnvironment( + cx, envChain); + if (!env) { + return false; + } + } + + return ExecuteInExtensibleLexicalEnvironment(cx, scriptArg, env); +} + +JS_PUBLIC_API JSObject* JS::GetJSMEnvironmentOfScriptedCaller(JSContext* cx) { + FrameIter iter(cx); + if (iter.done()) { + return nullptr; + } + + // WASM frames don't always provide their environment, but we also shouldn't + // expect to see any calling into here. + MOZ_RELEASE_ASSERT(!iter.isWasm()); + + RootedObject env(cx, iter.environmentChain(cx)); + while (env && !env->is<NonSyntacticVariablesObject>()) { + env = env->enclosingEnvironment(); + } + + return env; +} + +JS_PUBLIC_API bool JS::IsJSMEnvironment(JSObject* obj) { + // NOTE: This also returns true if the NonSyntacticVariablesObject was + // created for reasons other than the JSM loader. + return obj->is<NonSyntacticVariablesObject>(); +} + +#ifdef JSGC_HASH_TABLE_CHECKS +void RuntimeCaches::checkEvalCacheAfterMinorGC() { + JSContext* cx = TlsContext.get(); + for (auto r = evalCache.all(); !r.empty(); r.popFront()) { + const EvalCacheEntry& entry = r.front(); + CheckGCThingAfterMovingGC(entry.str); + EvalCacheLookup lookup(cx); + lookup.str = entry.str; + lookup.callerScript = entry.callerScript; + lookup.pc = entry.pc; + auto ptr = evalCache.lookup(lookup); + MOZ_RELEASE_ASSERT(ptr.found() && &*ptr == &r.front()); + } +} +#endif diff --git a/js/src/builtin/Eval.h b/js/src/builtin/Eval.h new file mode 100644 index 0000000000..4b027344de --- /dev/null +++ b/js/src/builtin/Eval.h @@ -0,0 +1,34 @@ +/* -*- 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 builtin_Eval_h +#define builtin_Eval_h + +#include "NamespaceImports.h" + +#include "js/TypeDecls.h" + +namespace js { + +// The C++ native for 'eval' (ES5 15.1.2.1). The function is named "indirect +// eval" because "direct eval" calls (as defined by the spec) will emit +// JSOp::Eval which in turn calls DirectEval. Thus, even though IndirectEval is +// the callee function object for *all* calls to eval, it is by construction +// only ever called in the case indirect eval. +[[nodiscard]] extern bool IndirectEval(JSContext* cx, unsigned argc, Value* vp); + +// Performs a direct eval of |v| (a string containing code, or another value +// that will be vacuously returned), which must correspond to the currently- +// executing stack frame, which must be a script frame. +[[nodiscard]] extern bool DirectEval(JSContext* cx, HandleValue v, + MutableHandleValue vp); + +// True iff fun is a built-in eval function. +extern bool IsAnyBuiltinEval(JSFunction* fun); + +} // namespace js + +#endif /* builtin_Eval_h */ diff --git a/js/src/builtin/FinalizationRegistryObject.cpp b/js/src/builtin/FinalizationRegistryObject.cpp new file mode 100644 index 0000000000..168c58aaf4 --- /dev/null +++ b/js/src/builtin/FinalizationRegistryObject.cpp @@ -0,0 +1,849 @@ +/* -*- 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/. */ + +// Implementation of JS FinalizationRegistry objects. + +#include "builtin/FinalizationRegistryObject.h" + +#include "mozilla/ScopeExit.h" + +#include "jsapi.h" + +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "vm/GlobalObject.h" +#include "vm/Interpreter.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "gc/GCContext-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +/////////////////////////////////////////////////////////////////////////// +// FinalizationRecordObject + +const JSClass FinalizationRecordObject::class_ = { + "FinalizationRecord", JSCLASS_HAS_RESERVED_SLOTS(SlotCount)}; + +/* static */ +FinalizationRecordObject* FinalizationRecordObject::create( + JSContext* cx, HandleFinalizationQueueObject queue, HandleValue heldValue) { + MOZ_ASSERT(queue); + + auto record = NewObjectWithGivenProto<FinalizationRecordObject>(cx, nullptr); + if (!record) { + return nullptr; + } + + MOZ_ASSERT(queue->compartment() == record->compartment()); + + record->initReservedSlot(QueueSlot, ObjectValue(*queue)); + record->initReservedSlot(HeldValueSlot, heldValue); + record->initReservedSlot(InMapSlot, BooleanValue(false)); + + return record; +} + +FinalizationQueueObject* FinalizationRecordObject::queue() const { + Value value = getReservedSlot(QueueSlot); + if (value.isUndefined()) { + return nullptr; + } + return &value.toObject().as<FinalizationQueueObject>(); +} + +Value FinalizationRecordObject::heldValue() const { + return getReservedSlot(HeldValueSlot); +} + +bool FinalizationRecordObject::isRegistered() const { + MOZ_ASSERT_IF(!queue(), heldValue().isUndefined()); + return queue(); +} + +bool FinalizationRecordObject::isInRecordMap() const { + return getReservedSlot(InMapSlot).toBoolean(); +} + +void FinalizationRecordObject::setInRecordMap(bool newValue) { + MOZ_ASSERT(newValue != isInRecordMap()); + setReservedSlot(InMapSlot, BooleanValue(newValue)); +} + +void FinalizationRecordObject::clear() { + MOZ_ASSERT(queue()); + setReservedSlot(QueueSlot, UndefinedValue()); + setReservedSlot(HeldValueSlot, UndefinedValue()); + MOZ_ASSERT(!isRegistered()); +} + +/////////////////////////////////////////////////////////////////////////// +// FinalizationRegistrationsObject + +const JSClass FinalizationRegistrationsObject::class_ = { + "FinalizationRegistrations", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | JSCLASS_BACKGROUND_FINALIZE, + &classOps_, JS_NULL_CLASS_SPEC}; + +const JSClassOps FinalizationRegistrationsObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + FinalizationRegistrationsObject::finalize, // finalize + nullptr, // call + nullptr, // construct + FinalizationRegistrationsObject::trace, // trace +}; + +/* static */ +FinalizationRegistrationsObject* FinalizationRegistrationsObject::create( + JSContext* cx) { + auto records = cx->make_unique<WeakFinalizationRecordVector>(cx->zone()); + if (!records) { + return nullptr; + } + + auto object = + NewObjectWithGivenProto<FinalizationRegistrationsObject>(cx, nullptr); + if (!object) { + return nullptr; + } + + InitReservedSlot(object, RecordsSlot, records.release(), + MemoryUse::FinalizationRecordVector); + + return object; +} + +/* static */ +void FinalizationRegistrationsObject::trace(JSTracer* trc, JSObject* obj) { + if (!trc->traceWeakEdges()) { + return; + } + + auto* self = &obj->as<FinalizationRegistrationsObject>(); + if (WeakFinalizationRecordVector* records = self->records()) { + TraceRange(trc, records->length(), records->begin(), + "FinalizationRegistrationsObject records"); + } +} + +/* static */ +void FinalizationRegistrationsObject::finalize(JS::GCContext* gcx, + JSObject* obj) { + auto* self = &obj->as<FinalizationRegistrationsObject>(); + gcx->delete_(obj, self->records(), MemoryUse::FinalizationRecordVector); +} + +inline WeakFinalizationRecordVector* +FinalizationRegistrationsObject::records() { + return static_cast<WeakFinalizationRecordVector*>(privatePtr()); +} + +inline const WeakFinalizationRecordVector* +FinalizationRegistrationsObject::records() const { + return static_cast<const WeakFinalizationRecordVector*>(privatePtr()); +} + +inline void* FinalizationRegistrationsObject::privatePtr() const { + Value value = getReservedSlot(RecordsSlot); + if (value.isUndefined()) { + return nullptr; + } + void* ptr = value.toPrivate(); + MOZ_ASSERT(ptr); + return ptr; +} + +inline bool FinalizationRegistrationsObject::isEmpty() const { + MOZ_ASSERT(records()); + return records()->empty(); +} + +inline bool FinalizationRegistrationsObject::append( + HandleFinalizationRecordObject record) { + MOZ_ASSERT(records()); + return records()->append(record); +} + +inline void FinalizationRegistrationsObject::remove( + HandleFinalizationRecordObject record) { + MOZ_ASSERT(records()); + records()->eraseIfEqual(record); +} + +inline bool FinalizationRegistrationsObject::traceWeak(JSTracer* trc) { + MOZ_ASSERT(records()); + return records()->traceWeak(trc); +} + +/////////////////////////////////////////////////////////////////////////// +// FinalizationRegistryObject + +// Bug 1600300: FinalizationRegistryObject is foreground finalized so that +// HeapPtr destructors never see referents with released arenas. When this is +// fixed we may be able to make this background finalized again. +const JSClass FinalizationRegistryObject::class_ = { + "FinalizationRegistry", + JSCLASS_HAS_CACHED_PROTO(JSProto_FinalizationRegistry) | + JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | JSCLASS_FOREGROUND_FINALIZE, + &classOps_, &classSpec_}; + +const JSClass FinalizationRegistryObject::protoClass_ = { + "FinalizationRegistry.prototype", + JSCLASS_HAS_CACHED_PROTO(JSProto_FinalizationRegistry), JS_NULL_CLASS_OPS, + &classSpec_}; + +const JSClassOps FinalizationRegistryObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + FinalizationRegistryObject::finalize, // finalize + nullptr, // call + nullptr, // construct + FinalizationRegistryObject::trace, // trace +}; + +const ClassSpec FinalizationRegistryObject::classSpec_ = { + GenericCreateConstructor<construct, 1, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<FinalizationRegistryObject>, + nullptr, + nullptr, + methods_, + properties_}; + +const JSFunctionSpec FinalizationRegistryObject::methods_[] = { + JS_FN(js_register_str, register_, 2, 0), + JS_FN(js_unregister_str, unregister, 1, 0), + JS_FN(js_cleanupSome_str, cleanupSome, 0, 0), JS_FS_END}; + +const JSPropertySpec FinalizationRegistryObject::properties_[] = { + JS_STRING_SYM_PS(toStringTag, "FinalizationRegistry", JSPROP_READONLY), + JS_PS_END}; + +/* static */ +bool FinalizationRegistryObject::construct(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "FinalizationRegistry")) { + return false; + } + + RootedObject cleanupCallback( + cx, ValueToCallable(cx, args.get(0), 1, NO_CONSTRUCT)); + if (!cleanupCallback) { + return false; + } + + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor( + cx, args, JSProto_FinalizationRegistry, &proto)) { + return false; + } + + Rooted<UniquePtr<ObjectWeakMap>> registrations( + cx, cx->make_unique<ObjectWeakMap>(cx)); + if (!registrations) { + return false; + } + + RootedFinalizationQueueObject queue( + cx, FinalizationQueueObject::create(cx, cleanupCallback)); + if (!queue) { + return false; + } + + RootedFinalizationRegistryObject registry( + cx, NewObjectWithClassProto<FinalizationRegistryObject>(cx, proto)); + if (!registry) { + return false; + } + + registry->initReservedSlot(QueueSlot, ObjectValue(*queue)); + InitReservedSlot(registry, RegistrationsSlot, registrations.release(), + MemoryUse::FinalizationRegistryRegistrations); + + if (!cx->runtime()->gc.addFinalizationRegistry(cx, registry)) { + return false; + } + + queue->setHasRegistry(true); + + args.rval().setObject(*registry); + return true; +} + +/* static */ +void FinalizationRegistryObject::trace(JSTracer* trc, JSObject* obj) { + // Trace the registrations weak map. At most this traces the + // FinalizationRegistrationsObject values of the map; the contents of those + // objects are weakly held and are not traced by this method. + + auto* registry = &obj->as<FinalizationRegistryObject>(); + if (ObjectWeakMap* registrations = registry->registrations()) { + registrations->trace(trc); + } +} + +void FinalizationRegistryObject::traceWeak(JSTracer* trc) { + // Trace and update the contents of the registrations weak map's values, which + // are weakly held. + + MOZ_ASSERT(registrations()); + for (ObjectValueWeakMap::Enum e(registrations()->valueMap()); !e.empty(); + e.popFront()) { + auto* registrations = + &e.front().value().toObject().as<FinalizationRegistrationsObject>(); + if (!registrations->traceWeak(trc)) { + e.removeFront(); + } + } +} + +/* static */ +void FinalizationRegistryObject::finalize(JS::GCContext* gcx, JSObject* obj) { + auto registry = &obj->as<FinalizationRegistryObject>(); + + // The queue's flag should have been updated by + // GCRuntime::sweepFinalizationRegistries. + MOZ_ASSERT_IF(registry->queue(), !registry->queue()->hasRegistry()); + + gcx->delete_(obj, registry->registrations(), + MemoryUse::FinalizationRegistryRegistrations); +} + +FinalizationQueueObject* FinalizationRegistryObject::queue() const { + Value value = getReservedSlot(QueueSlot); + if (value.isUndefined()) { + return nullptr; + } + return &value.toObject().as<FinalizationQueueObject>(); +} + +ObjectWeakMap* FinalizationRegistryObject::registrations() const { + Value value = getReservedSlot(RegistrationsSlot); + if (value.isUndefined()) { + return nullptr; + } + return static_cast<ObjectWeakMap*>(value.toPrivate()); +} + +// FinalizationRegistry.prototype.register(target, heldValue [, unregisterToken +// ]) +// https://tc39.es/proposal-weakrefs/#sec-finalization-registry.prototype.register +/* static */ +bool FinalizationRegistryObject::register_(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // 1. Let finalizationRegistry be the this value. + // 2. If Type(finalizationRegistry) is not Object, throw a TypeError + // exception. + // 3. If finalizationRegistry does not have a [[Cells]] internal slot, throw a + // TypeError exception. + if (!args.thisv().isObject() || + !args.thisv().toObject().is<FinalizationRegistryObject>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_A_FINALIZATION_REGISTRY, + "Receiver of FinalizationRegistry.register call"); + return false; + } + + RootedFinalizationRegistryObject registry( + cx, &args.thisv().toObject().as<FinalizationRegistryObject>()); + + // 4. If Type(target) is not Object, throw a TypeError exception. + if (!args.get(0).isObject()) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_OBJECT_REQUIRED, + "target argument to FinalizationRegistry.register"); + return false; + } + + RootedObject target(cx, &args[0].toObject()); + + // 5. If SameValue(target, heldValue), throw a TypeError exception. + if (args.get(1).isObject() && &args.get(1).toObject() == target) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_HELD_VALUE); + return false; + } + + HandleValue heldValue = args.get(1); + + // 6. If Type(unregisterToken) is not Object, + // a. If unregisterToken is not undefined, throw a TypeError exception. + if (!args.get(2).isUndefined() && !args.get(2).isObject()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_UNREGISTER_TOKEN, + "FinalizationRegistry.register"); + return false; + } + + RootedObject unregisterToken(cx); + if (!args.get(2).isUndefined()) { + unregisterToken = &args[2].toObject(); + } + + // Create the finalization record representing this target and heldValue. + Rooted<FinalizationQueueObject*> queue(cx, registry->queue()); + Rooted<FinalizationRecordObject*> record( + cx, FinalizationRecordObject::create(cx, queue, heldValue)); + if (!record) { + return false; + } + + // Add the record to the registrations if an unregister token was supplied. + if (unregisterToken && + !addRegistration(cx, registry, unregisterToken, record)) { + return false; + } + + auto registrationsGuard = mozilla::MakeScopeExit([&] { + if (unregisterToken) { + removeRegistrationOnError(registry, unregisterToken, record); + } + }); + + // Fully unwrap the target to pass it to the GC. + RootedObject unwrappedTarget(cx); + unwrappedTarget = CheckedUnwrapDynamic(target, cx); + if (!unwrappedTarget) { + ReportAccessDenied(cx); + return false; + } + + // If the target is a DOM wrapper, preserve it. + if (!preserveDOMWrapper(cx, target)) { + return false; + } + + // Wrap the record into the compartment of the target. + RootedObject wrappedRecord(cx, record); + AutoRealm ar(cx, unwrappedTarget); + if (!JS_WrapObject(cx, &wrappedRecord)) { + return false; + } + + if (JS_IsDeadWrapper(wrappedRecord)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEAD_OBJECT); + return false; + } + + // Register the record with the target. + gc::GCRuntime* gc = &cx->runtime()->gc; + if (!gc->registerWithFinalizationRegistry(cx, unwrappedTarget, + wrappedRecord)) { + return false; + } + + registrationsGuard.release(); + args.rval().setUndefined(); + return true; +} + +/* static */ +bool FinalizationRegistryObject::preserveDOMWrapper(JSContext* cx, + HandleObject obj) { + if (!MaybePreserveDOMWrapper(cx, obj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_FINALIZATION_REGISTRY_OBJECT); + return false; + } + + return true; +} + +/* static */ +bool FinalizationRegistryObject::addRegistration( + JSContext* cx, HandleFinalizationRegistryObject registry, + HandleObject unregisterToken, HandleFinalizationRecordObject record) { + // Add the record to the list of records associated with this unregister + // token. + + MOZ_ASSERT(unregisterToken); + MOZ_ASSERT(registry->registrations()); + + auto& map = *registry->registrations(); + Rooted<FinalizationRegistrationsObject*> recordsObject(cx); + JSObject* obj = map.lookup(unregisterToken); + if (obj) { + recordsObject = &obj->as<FinalizationRegistrationsObject>(); + } else { + recordsObject = FinalizationRegistrationsObject::create(cx); + if (!recordsObject || !map.add(cx, unregisterToken, recordsObject)) { + return false; + } + } + + if (!recordsObject->append(record)) { + ReportOutOfMemory(cx); + return false; + } + + return true; +} + +/* static */ void FinalizationRegistryObject::removeRegistrationOnError( + HandleFinalizationRegistryObject registry, HandleObject unregisterToken, + HandleFinalizationRecordObject record) { + // Remove a registration if something went wrong before we added it to the + // target zone's map. Note that this can't remove a registration after that + // point. + + MOZ_ASSERT(unregisterToken); + MOZ_ASSERT(registry->registrations()); + JS::AutoAssertNoGC nogc; + + auto& map = *registry->registrations(); + JSObject* obj = map.lookup(unregisterToken); + MOZ_ASSERT(obj); + auto records = &obj->as<FinalizationRegistrationsObject>(); + records->remove(record); + + if (records->empty()) { + map.remove(unregisterToken); + } +} + +// FinalizationRegistry.prototype.unregister ( unregisterToken ) +// https://tc39.es/proposal-weakrefs/#sec-finalization-registry.prototype.unregister +/* static */ +bool FinalizationRegistryObject::unregister(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // 1. Let finalizationRegistry be the this value. + // 2. If Type(finalizationRegistry) is not Object, throw a TypeError + // exception. + // 3. If finalizationRegistry does not have a [[Cells]] internal slot, throw a + // TypeError exception. + if (!args.thisv().isObject() || + !args.thisv().toObject().is<FinalizationRegistryObject>()) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_NOT_A_FINALIZATION_REGISTRY, + "Receiver of FinalizationRegistry.unregister call"); + return false; + } + + RootedFinalizationRegistryObject registry( + cx, &args.thisv().toObject().as<FinalizationRegistryObject>()); + + // 4. If Type(unregisterToken) is not Object, throw a TypeError exception. + if (!args.get(0).isObject()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_UNREGISTER_TOKEN, + "FinalizationRegistry.unregister"); + return false; + } + + RootedObject unregisterToken(cx, &args[0].toObject()); + + // 5. Let removed be false. + bool removed = false; + + // 6. For each Record { [[Target]], [[HeldValue]], [[UnregisterToken]] } cell + // that is an element of finalizationRegistry.[[Cells]], do + // a. If SameValue(cell.[[UnregisterToken]], unregisterToken) is true, then + // i. Remove cell from finalizationRegistry.[[Cells]]. + // ii. Set removed to true. + + RootedObject obj(cx, registry->registrations()->lookup(unregisterToken)); + if (obj) { + auto* records = obj->as<FinalizationRegistrationsObject>().records(); + MOZ_ASSERT(records); + MOZ_ASSERT(!records->empty()); + for (FinalizationRecordObject* record : *records) { + if (unregisterRecord(record)) { + removed = true; + } + } + registry->registrations()->remove(unregisterToken); + } + + // 7. Return removed. + args.rval().setBoolean(removed); + return true; +} + +/* static */ +bool FinalizationRegistryObject::unregisterRecord( + FinalizationRecordObject* record) { + if (!record->isRegistered()) { + return false; + } + + // Clear the fields of this record; it will be removed from the target's + // list when it is next swept. + record->clear(); + return true; +} + +// FinalizationRegistry.prototype.cleanupSome ( [ callback ] ) +// https://tc39.es/proposal-weakrefs/#sec-finalization-registry.prototype.cleanupSome +bool FinalizationRegistryObject::cleanupSome(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // 1. Let finalizationRegistry be the this value. + // 2. Perform ? RequireInternalSlot(finalizationRegistry, [[Cells]]). + if (!args.thisv().isObject() || + !args.thisv().toObject().is<FinalizationRegistryObject>()) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_NOT_A_FINALIZATION_REGISTRY, + "Receiver of FinalizationRegistry.cleanupSome call"); + return false; + } + + RootedFinalizationRegistryObject registry( + cx, &args.thisv().toObject().as<FinalizationRegistryObject>()); + + // 3. If callback is not undefined and IsCallable(callback) is false, throw a + // TypeError exception. + RootedObject cleanupCallback(cx); + if (!args.get(0).isUndefined()) { + cleanupCallback = ValueToCallable(cx, args.get(0), -1, NO_CONSTRUCT); + if (!cleanupCallback) { + return false; + } + } + + RootedFinalizationQueueObject queue(cx, registry->queue()); + if (!FinalizationQueueObject::cleanupQueuedRecords(cx, queue, + cleanupCallback)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +/////////////////////////////////////////////////////////////////////////// +// FinalizationQueueObject + +// Bug 1600300: FinalizationQueueObject is foreground finalized so that +// HeapPtr destructors never see referents with released arenas. When this is +// fixed we may be able to make this background finalized again. +const JSClass FinalizationQueueObject::class_ = { + "FinalizationQueue", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | JSCLASS_FOREGROUND_FINALIZE, + &classOps_}; + +const JSClassOps FinalizationQueueObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + FinalizationQueueObject::finalize, // finalize + nullptr, // call + nullptr, // construct + FinalizationQueueObject::trace, // trace +}; + +/* static */ +FinalizationQueueObject* FinalizationQueueObject::create( + JSContext* cx, HandleObject cleanupCallback) { + MOZ_ASSERT(cleanupCallback); + + Rooted<UniquePtr<FinalizationRecordVector>> recordsToBeCleanedUp( + cx, cx->make_unique<FinalizationRecordVector>(cx->zone())); + if (!recordsToBeCleanedUp) { + return nullptr; + } + + Handle<PropertyName*> funName = cx->names().empty; + RootedFunction doCleanupFunction( + cx, NewNativeFunction(cx, doCleanup, 0, funName, + gc::AllocKind::FUNCTION_EXTENDED)); + if (!doCleanupFunction) { + return nullptr; + } + + // It's problematic storing a CCW to a global in another compartment because + // you don't know how far to unwrap it to get the original object + // back. Instead store a CCW to a plain object in the same compartment as the + // global (this uses Object.prototype). + RootedObject incumbentObject(cx); + if (!GetObjectFromIncumbentGlobal(cx, &incumbentObject) || !incumbentObject) { + return nullptr; + } + + FinalizationQueueObject* queue = + NewObjectWithGivenProto<FinalizationQueueObject>(cx, nullptr); + if (!queue) { + return nullptr; + } + + queue->initReservedSlot(CleanupCallbackSlot, ObjectValue(*cleanupCallback)); + queue->initReservedSlot(IncumbentObjectSlot, ObjectValue(*incumbentObject)); + InitReservedSlot(queue, RecordsToBeCleanedUpSlot, + recordsToBeCleanedUp.release(), + MemoryUse::FinalizationRegistryRecordVector); + queue->initReservedSlot(IsQueuedForCleanupSlot, BooleanValue(false)); + queue->initReservedSlot(DoCleanupFunctionSlot, + ObjectValue(*doCleanupFunction)); + queue->initReservedSlot(HasRegistrySlot, BooleanValue(false)); + + doCleanupFunction->setExtendedSlot(DoCleanupFunction_QueueSlot, + ObjectValue(*queue)); + + return queue; +} + +/* static */ +void FinalizationQueueObject::trace(JSTracer* trc, JSObject* obj) { + auto queue = &obj->as<FinalizationQueueObject>(); + + if (FinalizationRecordVector* records = queue->recordsToBeCleanedUp()) { + records->trace(trc); + } +} + +/* static */ +void FinalizationQueueObject::finalize(JS::GCContext* gcx, JSObject* obj) { + auto queue = &obj->as<FinalizationQueueObject>(); + + gcx->delete_(obj, queue->recordsToBeCleanedUp(), + MemoryUse::FinalizationRegistryRecordVector); +} + +void FinalizationQueueObject::setHasRegistry(bool newValue) { + MOZ_ASSERT(hasRegistry() != newValue); + + // Suppress our assertions about touching grey things. It's OK for us to set a + // boolean slot even if this object is gray. + AutoTouchingGrayThings atgt; + + setReservedSlot(HasRegistrySlot, BooleanValue(newValue)); +} + +bool FinalizationQueueObject::hasRegistry() const { + return getReservedSlot(HasRegistrySlot).toBoolean(); +} + +inline JSObject* FinalizationQueueObject::cleanupCallback() const { + Value value = getReservedSlot(CleanupCallbackSlot); + if (value.isUndefined()) { + return nullptr; + } + return &value.toObject(); +} + +JSObject* FinalizationQueueObject::incumbentObject() const { + Value value = getReservedSlot(IncumbentObjectSlot); + if (value.isUndefined()) { + return nullptr; + } + return &value.toObject(); +} + +FinalizationRecordVector* FinalizationQueueObject::recordsToBeCleanedUp() + const { + Value value = getReservedSlot(RecordsToBeCleanedUpSlot); + if (value.isUndefined()) { + return nullptr; + } + return static_cast<FinalizationRecordVector*>(value.toPrivate()); +} + +bool FinalizationQueueObject::isQueuedForCleanup() const { + return getReservedSlot(IsQueuedForCleanupSlot).toBoolean(); +} + +JSFunction* FinalizationQueueObject::doCleanupFunction() const { + Value value = getReservedSlot(DoCleanupFunctionSlot); + if (value.isUndefined()) { + return nullptr; + } + return &value.toObject().as<JSFunction>(); +} + +void FinalizationQueueObject::queueRecordToBeCleanedUp( + FinalizationRecordObject* record) { + AutoEnterOOMUnsafeRegion oomUnsafe; + if (!recordsToBeCleanedUp()->append(record)) { + oomUnsafe.crash("FinalizationQueueObject::queueRecordsToBeCleanedUp"); + } +} + +void FinalizationQueueObject::setQueuedForCleanup(bool value) { + MOZ_ASSERT(value != isQueuedForCleanup()); + setReservedSlot(IsQueuedForCleanupSlot, BooleanValue(value)); +} + +/* static */ +bool FinalizationQueueObject::doCleanup(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedFunction callee(cx, &args.callee().as<JSFunction>()); + + Value value = callee->getExtendedSlot(DoCleanupFunction_QueueSlot); + RootedFinalizationQueueObject queue( + cx, &value.toObject().as<FinalizationQueueObject>()); + + queue->setQueuedForCleanup(false); + return cleanupQueuedRecords(cx, queue); +} + +// CleanupFinalizationRegistry ( finalizationRegistry [ , callback ] ) +// https://tc39.es/proposal-weakrefs/#sec-cleanup-finalization-registry +/* static */ +bool FinalizationQueueObject::cleanupQueuedRecords( + JSContext* cx, HandleFinalizationQueueObject queue, + HandleObject callbackArg) { + MOZ_ASSERT(cx->compartment() == queue->compartment()); + + // 2. If callback is undefined, set callback to + // finalizationRegistry.[[CleanupCallback]]. + RootedValue callback(cx); + if (callbackArg) { + callback.setObject(*callbackArg); + } else { + JSObject* cleanupCallback = queue->cleanupCallback(); + MOZ_ASSERT(cleanupCallback); + callback.setObject(*cleanupCallback); + } + + // 3. While finalizationRegistry.[[Cells]] contains a Record cell such that + // cell.[[WeakRefTarget]] is empty, then an implementation may perform the + // following steps, + // a. Choose any such cell. + // b. Remove cell from finalizationRegistry.[[Cells]]. + // c. Perform ? Call(callback, undefined, « cell.[[HeldValue]] »). + + RootedValue heldValue(cx); + RootedValue rval(cx); + FinalizationRecordVector* records = queue->recordsToBeCleanedUp(); + while (!records->empty()) { + FinalizationRecordObject* record = records->popCopy(); + + // Skip over records that have been unregistered. + if (!record->isRegistered()) { + continue; + } + + heldValue.set(record->heldValue()); + + record->clear(); + + if (!Call(cx, callback, UndefinedHandleValue, heldValue, &rval)) { + return false; + } + } + + return true; +} diff --git a/js/src/builtin/FinalizationRegistryObject.h b/js/src/builtin/FinalizationRegistryObject.h new file mode 100644 index 0000000000..288ebea424 --- /dev/null +++ b/js/src/builtin/FinalizationRegistryObject.h @@ -0,0 +1,274 @@ +/* -*- 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/. */ + +/* + * FinalizationRegistry objects allow a program to register to receive a + * callback after a 'target' object dies. The callback is passed a 'held value' + * (that hopefully doesn't entrain the target). An 'unregister token' is an + * object which can be used to remove multiple previous registrations in one go. + * + * To arrange this, the following data structures are used: + * + * +---------------------------------------+-------------------------------+ + * | FinalizationRegistry compartment | Target zone / compartment | + * | | | + * | +----------------------+ | +------------------+ | + * | +-----+ FinalizationRegistry | | | Zone | | + * | | +----------+-----------+ | +---------+--------+ | + * | | | | | | + * | | v | v | + * | | +-------------+-------------+ | +-------------+------------+ | + * | | | Registrations | | | FinalizationObservers | | + * | | | weak map | | +-------------+------------+ | + * | | +---------------------------+ | | | + * | | | Unregister : Records | | v | + * | | | token : object | | +------------+------------+ | + * | | +--------------------+------+ | | RecordMap map | | + * | | | | +-------------------------+ | + * | | v | | Target : Finalization | | + * | | +--------------------+------+ | | object : RecordVector | | + * | | | Finalization | | +----+-------------+------+ | + * | | | RegistrationsObject | | | | | + * | | +---------------------------+ | v v | + * | | | RecordVector | | +----+-----+ +----+-----+ | + * | | +-------------+-------------+ | | Target | | (CCW if | | + * | | | | | JSObject | | needed) | | + * | | * v | +----------+ +----+-----+ | + * | | +-------------+-------------+ * | | | + * | | | FinalizationRecordObject +<--------------------------+ | + * | | +---------------------------+ | | + * | | | Queue +--+ | | + * | | +---------------------------+ | | | + * | | | Held value | | | | + * | | +---------------------------+ | | | + * | | | | | + * | +--------------+ +--------------+ | | + * | | | | | + * | v v | | + * | +----------+---+----------+ | | + * | | FinalizationQueueObject | | | + * | +-------------------------+ | | + * | | | + * +---------------------------------------+-------------------------------+ + * + * A FinalizationRegistry consists of two parts: the FinalizationRegistry that + * consumers see and a FinalizationQueue used internally to queue and call the + * cleanup callbacks. + * + * Registering a target with a FinalizationRegistry creates a FinalizationRecord + * containing a pointer to the queue and the heldValue. This is added to a + * vector of records associated with the target, implemented as a map on the + * target's Zone. All finalization records are treated as GC roots. + * + * When a target is registered an unregister token may be supplied. If so, this + * is also recorded by the registry and is stored in a weak map of + * registrations. The values of this map are FinalizationRegistrationsObject + * objects. It's necessary to have another JSObject here because our weak map + * implementation only supports JS types as values. + * + * When targets are unregistered, the registration is looked up in the weakmap + * and the corresponding records are cleared. + + * The finalization record maps are swept during GC to check for records that + * have been cleared by unregistration, for FinalizationRecords that are dead + * and for nuked CCWs. In all cases the record is removed and the cleanup + * callback is not run. + * + * Following this the targets are checked to see if they are dying. For such + * targets the associated record list is processed and for each record the + * heldValue is queued on the FinalizationQueue. At a later time this causes the + * client's cleanup callback to be run. + */ + +#ifndef builtin_FinalizationRegistryObject_h +#define builtin_FinalizationRegistryObject_h + +#include "gc/Barrier.h" +#include "js/GCVector.h" +#include "vm/NativeObject.h" + +namespace js { + +class FinalizationRegistryObject; +class FinalizationRecordObject; +class FinalizationQueueObject; +class ObjectWeakMap; + +using HandleFinalizationRegistryObject = Handle<FinalizationRegistryObject*>; +using HandleFinalizationRecordObject = Handle<FinalizationRecordObject*>; +using HandleFinalizationQueueObject = Handle<FinalizationQueueObject*>; +using RootedFinalizationRegistryObject = Rooted<FinalizationRegistryObject*>; +using RootedFinalizationRecordObject = Rooted<FinalizationRecordObject*>; +using RootedFinalizationQueueObject = Rooted<FinalizationQueueObject*>; + +// A finalization record: a pair of finalization queue and held value. +// +// A finalization record represents the registered interest of a finalization +// registry in a target's finalization. +// +// Finalization records created in the 'registered' state but may be +// unregistered. This happens when: +// - the heldValue is passed to the registry's cleanup callback +// - the registry's unregister method removes the registration +// +// Finalization records are added to a per-zone record map. They are removed +// when the record is queued for cleanup, or if the interest in finalization is +// cancelled. See FinalizationObservers::shouldRemoveRecord for the possible +// reasons. + +class FinalizationRecordObject : public NativeObject { + enum { QueueSlot = 0, HeldValueSlot, InMapSlot, SlotCount }; + + public: + static const JSClass class_; + + static FinalizationRecordObject* create(JSContext* cx, + HandleFinalizationQueueObject queue, + HandleValue heldValue); + + FinalizationQueueObject* queue() const; + Value heldValue() const; + bool isRegistered() const; + bool isInRecordMap() const; + + void setInRecordMap(bool newValue); + void clear(); +}; + +// A vector of weakly-held FinalizationRecordObjects. +using WeakFinalizationRecordVector = + GCVector<WeakHeapPtr<FinalizationRecordObject*>, 1, js::CellAllocPolicy>; + +// A JS object containing a vector of weakly-held FinalizationRecordObjects, +// which holds the records corresponding to the registrations for a particular +// registration token. These are used as the values in the registration +// weakmap. Since the contents of the vector are weak references they are not +// traced. +class FinalizationRegistrationsObject : public NativeObject { + enum { RecordsSlot = 0, SlotCount }; + + public: + static const JSClass class_; + + static FinalizationRegistrationsObject* create(JSContext* cx); + + WeakFinalizationRecordVector* records(); + const WeakFinalizationRecordVector* records() const; + + bool isEmpty() const; + + bool append(HandleFinalizationRecordObject record); + void remove(HandleFinalizationRecordObject record); + + bool traceWeak(JSTracer* trc); + + private: + static const JSClassOps classOps_; + + void* privatePtr() const; + + static void trace(JSTracer* trc, JSObject* obj); + static void finalize(JS::GCContext* gcx, JSObject* obj); +}; + +using FinalizationRecordVector = + GCVector<HeapPtr<FinalizationRecordObject*>, 1, js::CellAllocPolicy>; + +// The JS FinalizationRegistry object itself. +class FinalizationRegistryObject : public NativeObject { + enum { QueueSlot = 0, RegistrationsSlot, SlotCount }; + + public: + static const JSClass class_; + static const JSClass protoClass_; + + FinalizationQueueObject* queue() const; + ObjectWeakMap* registrations() const; + + void traceWeak(JSTracer* trc); + + static bool unregisterRecord(FinalizationRecordObject* record); + + static bool cleanupQueuedRecords(JSContext* cx, + HandleFinalizationRegistryObject registry, + HandleObject callback = nullptr); + + private: + static const JSClassOps classOps_; + static const ClassSpec classSpec_; + static const JSFunctionSpec methods_[]; + static const JSPropertySpec properties_[]; + + static bool construct(JSContext* cx, unsigned argc, Value* vp); + static bool register_(JSContext* cx, unsigned argc, Value* vp); + static bool unregister(JSContext* cx, unsigned argc, Value* vp); + static bool cleanupSome(JSContext* cx, unsigned argc, Value* vp); + + static bool addRegistration(JSContext* cx, + HandleFinalizationRegistryObject registry, + HandleObject unregisterToken, + HandleFinalizationRecordObject record); + static void removeRegistrationOnError( + HandleFinalizationRegistryObject registry, HandleObject unregisterToken, + HandleFinalizationRecordObject record); + + static bool preserveDOMWrapper(JSContext* cx, HandleObject obj); + + static void trace(JSTracer* trc, JSObject* obj); + static void finalize(JS::GCContext* gcx, JSObject* obj); +}; + +// Contains information about the cleanup callback and the records queued to +// be cleaned up. This is not exposed to content JS. +class FinalizationQueueObject : public NativeObject { + enum { + CleanupCallbackSlot = 0, + IncumbentObjectSlot, + RecordsToBeCleanedUpSlot, + IsQueuedForCleanupSlot, + DoCleanupFunctionSlot, + HasRegistrySlot, + SlotCount + }; + + enum DoCleanupFunctionSlots { + DoCleanupFunction_QueueSlot = 0, + }; + + public: + static const JSClass class_; + + JSObject* cleanupCallback() const; + JSObject* incumbentObject() const; + FinalizationRecordVector* recordsToBeCleanedUp() const; + bool isQueuedForCleanup() const; + JSFunction* doCleanupFunction() const; + bool hasRegistry() const; + + void queueRecordToBeCleanedUp(FinalizationRecordObject* record); + void setQueuedForCleanup(bool value); + + void setHasRegistry(bool newValue); + + static FinalizationQueueObject* create(JSContext* cx, + HandleObject cleanupCallback); + + static bool cleanupQueuedRecords(JSContext* cx, + HandleFinalizationQueueObject registry, + HandleObject callback = nullptr); + + private: + static const JSClassOps classOps_; + + static bool doCleanup(JSContext* cx, unsigned argc, Value* vp); + + static void trace(JSTracer* trc, JSObject* obj); + static void finalize(JS::GCContext* gcx, JSObject* obj); +}; + +} // namespace js + +#endif /* builtin_FinalizationRegistryObject_h */ diff --git a/js/src/builtin/Generator.js b/js/src/builtin/Generator.js new file mode 100644 index 0000000000..ada80e3006 --- /dev/null +++ b/js/src/builtin/Generator.js @@ -0,0 +1,114 @@ +/* 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/. */ + +function GeneratorNext(val) { + // The IsSuspendedGenerator call below is not necessary for correctness. + // It's a performance optimization to check for the common case with a + // single call. It's also inlined in Baseline. + + if (!IsSuspendedGenerator(this)) { + if (!IsObject(this) || !IsGeneratorObject(this)) { + return callFunction( + CallGeneratorMethodIfWrapped, + this, + val, + "GeneratorNext" + ); + } + + if (GeneratorObjectIsClosed(this)) { + return { value: undefined, done: true }; + } + + if (GeneratorIsRunning(this)) { + ThrowTypeError(JSMSG_NESTING_GENERATOR); + } + } + + try { + return resumeGenerator(this, val, "next"); + } catch (e) { + if (!GeneratorObjectIsClosed(this)) { + GeneratorSetClosed(this); + } + throw e; + } +} + +function GeneratorThrow(val) { + if (!IsSuspendedGenerator(this)) { + if (!IsObject(this) || !IsGeneratorObject(this)) { + return callFunction( + CallGeneratorMethodIfWrapped, + this, + val, + "GeneratorThrow" + ); + } + + if (GeneratorObjectIsClosed(this)) { + throw val; + } + + if (GeneratorIsRunning(this)) { + ThrowTypeError(JSMSG_NESTING_GENERATOR); + } + } + + try { + return resumeGenerator(this, val, "throw"); + } catch (e) { + if (!GeneratorObjectIsClosed(this)) { + GeneratorSetClosed(this); + } + throw e; + } +} + +function GeneratorReturn(val) { + if (!IsSuspendedGenerator(this)) { + if (!IsObject(this) || !IsGeneratorObject(this)) { + return callFunction( + CallGeneratorMethodIfWrapped, + this, + val, + "GeneratorReturn" + ); + } + + if (GeneratorObjectIsClosed(this)) { + return { value: val, done: true }; + } + + if (GeneratorIsRunning(this)) { + ThrowTypeError(JSMSG_NESTING_GENERATOR); + } + } + + try { + var rval = { value: val, done: true }; + return resumeGenerator(this, rval, "return"); + } catch (e) { + if (!GeneratorObjectIsClosed(this)) { + GeneratorSetClosed(this); + } + throw e; + } +} + +function InterpretGeneratorResume(gen, val, kind) { + // If we want to resume a generator in the interpreter, the script containing + // the resumeGenerator/JSOp::Resume also has to run in the interpreter. The + // forceInterpreter() call below compiles to a bytecode op that prevents us + // from JITing this script. + forceInterpreter(); + if (kind === "next") { + return resumeGenerator(gen, val, "next"); + } + if (kind === "throw") { + return resumeGenerator(gen, val, "throw"); + } + assert(kind === "return", "Invalid resume kind"); + return resumeGenerator(gen, val, "return"); +} diff --git a/js/src/builtin/HandlerFunction-inl.h b/js/src/builtin/HandlerFunction-inl.h new file mode 100644 index 0000000000..baaf3d2777 --- /dev/null +++ b/js/src/builtin/HandlerFunction-inl.h @@ -0,0 +1,110 @@ +/* -*- 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/. */ + +/* + * Handler for operations that act on a target object, and possibly upon + * an extra value. + */ + +#ifndef builtin_HandlerFunction_inl_h +#define builtin_HandlerFunction_inl_h + +#include <stddef.h> // size_t + +#include "gc/AllocKind.h" // js::gc::AllocKind +#include "js/CallArgs.h" // JS::CallArgs +#include "js/RootingAPI.h" // JS::Handle, JS::Rooted +#include "js/Value.h" // JS::ObjectValue +#include "vm/JSContext.h" // JSContext +#include "vm/JSFunction.h" // JSFunction, js::Native, js::NewNativeFunction +#include "vm/JSObject.h" // JSObject, js::GenericObject +#include "vm/StringType.h" // js::PropertyName + +#include "vm/JSContext-inl.h" // JSContext::check + +namespace js { + +// Handler functions are extended functions, that close over a target object and +// (optionally) over an extra object, storing those objects in the function's +// extended slots. +constexpr size_t HandlerFunctionSlot_Target = 0; +constexpr size_t HandlerFunctionSlot_Extra = 1; + +static_assert(HandlerFunctionSlot_Extra < FunctionExtended::NUM_EXTENDED_SLOTS, + "handler function slots shouldn't exceed available extended " + "slots"); + +[[nodiscard]] inline JSFunction* NewHandler(JSContext* cx, Native handler, + JS::Handle<JSObject*> target) { + cx->check(target); + + JS::Handle<PropertyName*> funName = cx->names().empty; + JS::Rooted<JSFunction*> handlerFun( + cx, NewNativeFunction(cx, handler, 0, funName, + gc::AllocKind::FUNCTION_EXTENDED, GenericObject)); + if (!handlerFun) { + return nullptr; + } + handlerFun->setExtendedSlot(HandlerFunctionSlot_Target, + JS::ObjectValue(*target)); + return handlerFun; +} + +[[nodiscard]] inline JSFunction* NewHandlerWithExtra( + JSContext* cx, Native handler, JS::Handle<JSObject*> target, + JS::Handle<JSObject*> extra) { + cx->check(extra); + JSFunction* handlerFun = NewHandler(cx, handler, target); + if (handlerFun) { + handlerFun->setExtendedSlot(HandlerFunctionSlot_Extra, + JS::ObjectValue(*extra)); + } + return handlerFun; +} + +[[nodiscard]] inline JSFunction* NewHandlerWithExtraValue( + JSContext* cx, Native handler, JS::Handle<JSObject*> target, + JS::Handle<JS::Value> extra) { + cx->check(extra); + JSFunction* handlerFun = NewHandler(cx, handler, target); + if (handlerFun) { + handlerFun->setExtendedSlot(HandlerFunctionSlot_Extra, extra); + } + return handlerFun; +} + +/** + * Within the call of a handler function that "closes over" a target value that + * is always a |T*| object (and never a wrapper around one), return that |T*|. + */ +template <class T> +[[nodiscard]] inline T* TargetFromHandler(const JS::CallArgs& args) { + JSFunction& func = args.callee().as<JSFunction>(); + return &func.getExtendedSlot(HandlerFunctionSlot_Target).toObject().as<T>(); +} + +/** + * Within the call of a handler function that "closes over" a target value and + * an extra value, return that extra value. + */ +[[nodiscard]] inline JS::Value ExtraValueFromHandler(const JS::CallArgs& args) { + JSFunction& func = args.callee().as<JSFunction>(); + return func.getExtendedSlot(HandlerFunctionSlot_Extra); +} + +/** + * Within the call of a handler function that "closes over" a target value and + * an extra value, where that extra value is always a |T*| object (and never a + * wrapper around one), return that |T*|. + */ +template <class T> +[[nodiscard]] inline T* ExtraFromHandler(const JS::CallArgs& args) { + return &ExtraValueFromHandler(args).toObject().as<T>(); +} + +} // namespace js + +#endif // builtin_HandlerFunction_inl_h diff --git a/js/src/builtin/Iterator.js b/js/src/builtin/Iterator.js new file mode 100644 index 0000000000..c759691782 --- /dev/null +++ b/js/src/builtin/Iterator.js @@ -0,0 +1,785 @@ +/* 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/. */ + +function IteratorIdentity() { + return this; +} + +/* ECMA262 7.2.7 */ +function IteratorNext(iteratorRecord, value) { + // Steps 1-2. + const result = + ArgumentsLength() < 2 + ? callContentFunction(iteratorRecord.nextMethod, iteratorRecord.iterator) + : callContentFunction( + iteratorRecord.nextMethod, + iteratorRecord.iterator, + value + ); + // Step 3. + if (!IsObject(result)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, result); + } + // Step 4. + return result; +} + +/* ECMA262 7.4.6 */ +function IteratorClose(iteratorRecord, value) { + // Step 3. + const iterator = iteratorRecord.iterator; + // Step 4. + const returnMethod = iterator.return; + // Step 5. + if (!IsNullOrUndefined(returnMethod)) { + const result = callContentFunction(returnMethod, iterator); + // Step 8. + if (!IsObject(result)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, DecompileArg(0, result)); + } + } + // Step 5b & 9. + return value; +} + +/** + * ES2022 draft rev c5f683e61d5dce703650f1c90d2309c46f8c157a + * + * GetIterator ( obj [ , hint [ , method ] ] ) + * https://tc39.es/ecma262/#sec-getiterator + * + * Optimized for single argument + */ +function GetIteratorSync(obj) { + // Steps 1 & 2 skipped as we know we want the sync iterator method + var method = GetMethod(obj, GetBuiltinSymbol("iterator")); + + // Step 3. Let iterator be ? Call(method, obj). + var iterator = callContentFunction(method, obj); + + // Step 4. If Type(iterator) is not Object, throw a TypeError exception. + if (!IsObject(iterator)) { + ThrowTypeError(JSMSG_NOT_ITERABLE, obj === null ? "null" : typeof obj); + } + + // Step 5. Let nextMethod be ? GetV(iterator, "next"). + var nextMethod = iterator.next; + + // Step 6. Let iteratorRecord be the Record { [[Iterator]]: iterator, [[NextMethod]]: nextMethod, [[Done]]: false }. + var iteratorRecord = { + iterator, + nextMethod, + done: false, + }; + + // Step 7. Return iteratorRecord. + return iteratorRecord; +} + +/* Iterator Helpers proposal 1.1.1 */ +function GetIteratorDirect(obj) { + // Step 1. + if (!IsObject(obj)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, DecompileArg(0, obj)); + } + + // Step 2. + const nextMethod = obj.next; + // Step 3. + if (!IsCallable(nextMethod)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, nextMethod)); + } + + // Steps 4-5. + return { + iterator: obj, + nextMethod, + done: false, + }; +} + +function GetIteratorDirectWrapper(obj) { + // Step 1. + if (!IsObject(obj)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, obj); + } + + // Step 2. + const nextMethod = obj.next; + // Step 3. + if (!IsCallable(nextMethod)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, nextMethod); + } + + // Steps 4-5. + return { + // Use a named function expression instead of a method definition, so + // we don't create an inferred name for this function at runtime. + [GetBuiltinSymbol("iterator")]: function IteratorMethod() { + return this; + }, + next(value) { + return callContentFunction(nextMethod, obj, value); + }, + return(value) { + const returnMethod = obj.return; + if (!IsNullOrUndefined(returnMethod)) { + return callContentFunction(returnMethod, obj, value); + } + return { done: true, value }; + }, + }; +} + +/* Iterator Helpers proposal 1.1.2 */ +function IteratorStep(iteratorRecord, value) { + // Steps 2-3. + let result; + if (ArgumentsLength() === 2) { + result = callContentFunction( + iteratorRecord.nextMethod, + iteratorRecord.iterator, + value + ); + } else { + result = callContentFunction( + iteratorRecord.nextMethod, + iteratorRecord.iterator + ); + } + + // IteratorNext Step 3. + if (!IsObject(result)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, DecompileArg(0, result)); + } + + // Steps 4-6. + return result.done ? false : result; +} + +/* Iterator Helpers proposal 2.1.3.3.1 */ +function IteratorFrom(O) { + // Step 1. + const usingIterator = O[GetBuiltinSymbol("iterator")]; + + let iteratorRecord; + // Step 2. + if (!IsNullOrUndefined(usingIterator)) { + // Step a. + // Inline call to GetIterator. + const iterator = callContentFunction(usingIterator, O); + iteratorRecord = GetIteratorDirect(iterator); + // Step b-c. + if (iteratorRecord.iterator instanceof GetBuiltinConstructor("Iterator")) { + return iteratorRecord.iterator; + } + } else { + // Step 3. + iteratorRecord = GetIteratorDirect(O); + } + + // Step 4. + const wrapper = NewWrapForValidIterator(); + // Step 5. + UnsafeSetReservedSlot(wrapper, ITERATED_SLOT, iteratorRecord); + // Step 6. + return wrapper; +} + +/* Iterator Helpers proposal 2.1.3.3.1.1.1 */ +function WrapForValidIteratorNext(value) { + // Step 1-2. + let O = this; + if (!IsObject(O) || (O = GuardToWrapForValidIterator(O)) === null) { + if (ArgumentsLength() === 0) { + return callFunction( + CallWrapForValidIteratorMethodIfWrapped, + this, + "WrapForValidIteratorNext" + ); + } + return callFunction( + CallWrapForValidIteratorMethodIfWrapped, + this, + value, + "WrapForValidIteratorNext" + ); + } + const iterated = UnsafeGetReservedSlot(O, ITERATED_SLOT); + // Step 3. + let result; + if (ArgumentsLength() === 0) { + result = callContentFunction(iterated.nextMethod, iterated.iterator); + } else { + // Step 4. + result = callContentFunction(iterated.nextMethod, iterated.iterator, value); + } + // Inlined from IteratorNext. + if (!IsObject(result)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, DecompileArg(0, result)); + } + return result; +} + +/* Iterator Helpers proposal 2.1.3.3.1.1.2 */ +function WrapForValidIteratorReturn(value) { + // Step 1-2. + let O = this; + if (!IsObject(O) || (O = GuardToWrapForValidIterator(O)) === null) { + return callFunction( + CallWrapForValidIteratorMethodIfWrapped, + this, + value, + "WrapForValidIteratorReturn" + ); + } + const iterated = UnsafeGetReservedSlot(O, ITERATED_SLOT); + + // Step 3. + // Inline call to IteratorClose. + const iterator = iterated.iterator; + const returnMethod = iterator.return; + if (!IsNullOrUndefined(returnMethod)) { + let innerResult = callContentFunction(returnMethod, iterator); + if (!IsObject(innerResult)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, DecompileArg(0, innerResult)); + } + } + // Step 4. + return { + done: true, + value, + }; +} + +/* Iterator Helpers proposal 2.1.3.3.1.1.3 */ +function WrapForValidIteratorThrow(value) { + // Step 1-2. + let O = this; + if (!IsObject(O) || (O = GuardToWrapForValidIterator(O)) === null) { + return callFunction( + CallWrapForValidIteratorMethodIfWrapped, + this, + value, + "WrapForValidIteratorThrow" + ); + } + const iterated = UnsafeGetReservedSlot(O, ITERATED_SLOT); + // Step 3. + const iterator = iterated.iterator; + // Step 4. + const throwMethod = iterator.throw; + // Step 5. + if (IsNullOrUndefined(throwMethod)) { + throw value; + } + // Step 6. + return callContentFunction(throwMethod, iterator, value); +} + +/* Iterator Helper object prototype methods. */ +function IteratorHelperNext(value) { + let O = this; + if (!IsObject(O) || (O = GuardToIteratorHelper(O)) === null) { + return callFunction( + CallIteratorHelperMethodIfWrapped, + this, + value, + "IteratorHelperNext" + ); + } + const generator = UnsafeGetReservedSlot(O, ITERATOR_HELPER_GENERATOR_SLOT); + return callContentFunction(GeneratorNext, generator, value); +} + +function IteratorHelperReturn(value) { + let O = this; + if (!IsObject(O) || (O = GuardToIteratorHelper(O)) === null) { + return callFunction( + CallIteratorHelperMethodIfWrapped, + this, + value, + "IteratorHelperReturn" + ); + } + const generator = UnsafeGetReservedSlot(O, ITERATOR_HELPER_GENERATOR_SLOT); + return callContentFunction(GeneratorReturn, generator, value); +} + +function IteratorHelperThrow(value) { + let O = this; + if (!IsObject(O) || (O = GuardToIteratorHelper(O)) === null) { + return callFunction( + CallIteratorHelperMethodIfWrapped, + this, + value, + "IteratorHelperThrow" + ); + } + const generator = UnsafeGetReservedSlot(O, ITERATOR_HELPER_GENERATOR_SLOT); + return callContentFunction(GeneratorThrow, generator, value); +} + +// Lazy %Iterator.prototype% methods +// Iterator Helpers proposal 2.1.5.2-2.1.5.7 +// +// In order to match the semantics of the built-in generator objects used in +// the proposal, we use a reserved slot on the IteratorHelper objects to store +// a regular generator that is called from the %IteratorHelper.prototype% +// methods. +// +// Each of the lazy methods is divided into a prelude and a body, with the +// eager prelude steps being contained in the corresponding IteratorX method +// and the lazy body steps inside the IteratorXGenerator generator functions. +// +// Each prelude method initializes and returns a new IteratorHelper object. +// As part of this initialization process, the appropriate generator function +// is called, followed by GeneratorNext being called on returned generator +// instance in order to move it to it's first yield point. This is done so that +// if the return or throw methods are called on the IteratorHelper before next +// has been called, we can catch them in the try and use the finally block to +// close the source iterator. +// +// The needClose flag is used to track when the source iterator should be closed +// following an exception being thrown within the generator, corresponding to +// whether or not the abrupt completions in the spec are being passed back to +// the caller (when needClose is false) or handled with IfAbruptCloseIterator +// (when needClose is true). + +/* Iterator Helpers proposal 2.1.5.2 Prelude */ +function IteratorMap(mapper) { + // Step 1. + const iterated = GetIteratorDirect(this); + + // Step 2. + if (!IsCallable(mapper)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, mapper)); + } + + const iteratorHelper = NewIteratorHelper(); + const generator = IteratorMapGenerator(iterated, mapper); + callContentFunction(GeneratorNext, generator); + UnsafeSetReservedSlot( + iteratorHelper, + ITERATOR_HELPER_GENERATOR_SLOT, + generator + ); + return iteratorHelper; +} + +/* Iterator Helpers proposal 2.1.5.2 Body */ +function* IteratorMapGenerator(iterated, mapper) { + // Step 1. + let lastValue; + // Step 2. + let needClose = true; + try { + yield; + needClose = false; + + for ( + let next = IteratorStep(iterated, lastValue); + next; + next = IteratorStep(iterated, lastValue) + ) { + // Step c. + const value = next.value; + + // Steps d-g. + needClose = true; + lastValue = yield callContentFunction(mapper, undefined, value); + needClose = false; + } + } finally { + if (needClose) { + IteratorClose(iterated); + } + } +} + +/* Iterator Helpers proposal 2.1.5.3 Prelude */ +function IteratorFilter(filterer) { + // Step 1. + const iterated = GetIteratorDirect(this); + + // Step 2. + if (!IsCallable(filterer)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, filterer)); + } + + const iteratorHelper = NewIteratorHelper(); + const generator = IteratorFilterGenerator(iterated, filterer); + callContentFunction(GeneratorNext, generator); + UnsafeSetReservedSlot( + iteratorHelper, + ITERATOR_HELPER_GENERATOR_SLOT, + generator + ); + return iteratorHelper; +} + +/* Iterator Helpers proposal 2.1.5.3 Body */ +function* IteratorFilterGenerator(iterated, filterer) { + // Step 1. + let lastValue; + // Step 2. + let needClose = true; + try { + yield; + needClose = false; + + for ( + let next = IteratorStep(iterated, lastValue); + next; + next = IteratorStep(iterated, lastValue) + ) { + // Step c. + const value = next.value; + + // Steps d-g. + needClose = true; + if (callContentFunction(filterer, undefined, value)) { + lastValue = yield value; + } + needClose = false; + } + } finally { + if (needClose) { + IteratorClose(iterated); + } + } +} + +/* Iterator Helpers proposal 2.1.5.4 Prelude */ +function IteratorTake(limit) { + // Step 1. + const iterated = GetIteratorDirect(this); + + // Step 2. + const remaining = ToInteger(limit); + // Step 3. + if (remaining < 0) { + ThrowRangeError(JSMSG_NEGATIVE_LIMIT); + } + + const iteratorHelper = NewIteratorHelper(); + const generator = IteratorTakeGenerator(iterated, remaining); + callContentFunction(GeneratorNext, generator); + UnsafeSetReservedSlot( + iteratorHelper, + ITERATOR_HELPER_GENERATOR_SLOT, + generator + ); + return iteratorHelper; +} + +/* Iterator Helpers proposal 2.1.5.4 Body */ +function* IteratorTakeGenerator(iterated, remaining) { + // Step 1. + let lastValue; + // Step 2. + let needClose = true; + try { + yield; + needClose = false; + + for (; remaining > 0; remaining--) { + const next = IteratorStep(iterated, lastValue); + if (!next) { + return; + } + + const value = next.value; + needClose = true; + lastValue = yield value; + needClose = false; + } + } finally { + if (needClose) { + IteratorClose(iterated); + } + } + + IteratorClose(iterated); +} + +/* Iterator Helpers proposal 2.1.5.5 Prelude */ +function IteratorDrop(limit) { + // Step 1. + const iterated = GetIteratorDirect(this); + + // Step 2. + const remaining = ToInteger(limit); + // Step 3. + if (remaining < 0) { + ThrowRangeError(JSMSG_NEGATIVE_LIMIT); + } + + const iteratorHelper = NewIteratorHelper(); + const generator = IteratorDropGenerator(iterated, remaining); + callContentFunction(GeneratorNext, generator); + UnsafeSetReservedSlot( + iteratorHelper, + ITERATOR_HELPER_GENERATOR_SLOT, + generator + ); + return iteratorHelper; +} + +/* Iterator Helpers proposal 2.1.5.5 Body */ +function* IteratorDropGenerator(iterated, remaining) { + let needClose = true; + try { + yield; + needClose = false; + + // Step 1. + for (; remaining > 0; remaining--) { + if (!IteratorStep(iterated)) { + return; + } + } + + // Step 2. + let lastValue; + // Step 3. + for ( + let next = IteratorStep(iterated, lastValue); + next; + next = IteratorStep(iterated, lastValue) + ) { + // Steps c-d. + const value = next.value; + + needClose = true; + lastValue = yield value; + needClose = false; + } + } finally { + if (needClose) { + IteratorClose(iterated); + } + } +} + +/* Iterator Helpers proposal 2.1.5.6 Prelude */ +function IteratorAsIndexedPairs() { + // Step 1. + const iterated = GetIteratorDirect(this); + + const iteratorHelper = NewIteratorHelper(); + const generator = IteratorAsIndexedPairsGenerator(iterated); + callContentFunction(GeneratorNext, generator); + UnsafeSetReservedSlot( + iteratorHelper, + ITERATOR_HELPER_GENERATOR_SLOT, + generator + ); + return iteratorHelper; +} + +/* Iterator Helpers proposal 2.1.5.6 Body */ +function* IteratorAsIndexedPairsGenerator(iterated) { + // Step 2. + let lastValue; + // Step 3. + let needClose = true; + try { + yield; + needClose = false; + + for ( + let next = IteratorStep(iterated, lastValue), index = 0; + next; + next = IteratorStep(iterated, lastValue), index++ + ) { + // Steps c-d. + const value = next.value; + + needClose = true; + lastValue = yield [index, value]; + needClose = false; + } + } finally { + if (needClose) { + IteratorClose(iterated); + } + } +} + +/* Iterator Helpers proposal 2.1.5.7 Prelude */ +function IteratorFlatMap(mapper) { + // Step 1. + const iterated = GetIteratorDirect(this); + + // Step 2. + if (!IsCallable(mapper)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, mapper)); + } + + const iteratorHelper = NewIteratorHelper(); + const generator = IteratorFlatMapGenerator(iterated, mapper); + callContentFunction(GeneratorNext, generator); + UnsafeSetReservedSlot( + iteratorHelper, + ITERATOR_HELPER_GENERATOR_SLOT, + generator + ); + return iteratorHelper; +} + +/* Iterator Helpers proposal 2.1.5.7 Body */ +function* IteratorFlatMapGenerator(iterated, mapper) { + // Step 1. + let needClose = true; + try { + yield; + needClose = false; + + for ( + let next = IteratorStep(iterated); + next; + next = IteratorStep(iterated) + ) { + // Step c. + const value = next.value; + + needClose = true; + // Step d. + const mapped = callContentFunction(mapper, undefined, value); + // Steps f-i. + for (const innerValue of allowContentIter(mapped)) { + yield innerValue; + } + needClose = false; + } + } finally { + if (needClose) { + IteratorClose(iterated); + } + } +} + +/* Iterator Helpers proposal 2.1.5.8 */ +function IteratorReduce(reducer /*, initialValue*/) { + // Step 1. + const iterated = GetIteratorDirectWrapper(this); + + // Step 2. + if (!IsCallable(reducer)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, reducer)); + } + + // Step 3. + let accumulator; + if (ArgumentsLength() === 1) { + // Step a. + const next = callContentFunction(iterated.next, iterated); + if (!IsObject(next)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, DecompileArg(0, next)); + } + // Step b. + if (next.done) { + ThrowTypeError(JSMSG_EMPTY_ITERATOR_REDUCE); + } + // Step c. + accumulator = next.value; + } else { + // Step 4. + accumulator = GetArgument(1); + } + + // Step 5. + for (const value of allowContentIter(iterated)) { + accumulator = callContentFunction(reducer, undefined, accumulator, value); + } + return accumulator; +} + +/* Iterator Helpers proposal 2.1.5.9 */ +function IteratorToArray() { + // Step 1. + const iterated = { + [GetBuiltinSymbol("iterator")]: () => this, + }; + // Steps 2-3. + return [...allowContentIter(iterated)]; +} + +/* Iterator Helpers proposal 2.1.5.10 */ +function IteratorForEach(fn) { + // Step 1. + const iterated = GetIteratorDirectWrapper(this); + + // Step 2. + if (!IsCallable(fn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, fn)); + } + + // Step 3. + for (const value of allowContentIter(iterated)) { + callContentFunction(fn, undefined, value); + } +} + +/* Iterator Helpers proposal 2.1.5.11 */ +function IteratorSome(fn) { + // Step 1. + const iterated = GetIteratorDirectWrapper(this); + + // Step 2. + if (!IsCallable(fn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, fn)); + } + + // Step 3. + for (const value of allowContentIter(iterated)) { + // Steps d-f. + if (callContentFunction(fn, undefined, value)) { + return true; + } + } + // Step 3b. + return false; +} + +/* Iterator Helpers proposal 2.1.5.12 */ +function IteratorEvery(fn) { + // Step 1. + const iterated = GetIteratorDirectWrapper(this); + + // Step 2. + if (!IsCallable(fn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, fn)); + } + + // Step 3. + for (const value of allowContentIter(iterated)) { + // Steps d-f. + if (!callContentFunction(fn, undefined, value)) { + return false; + } + } + // Step 3b. + return true; +} + +/* Iterator Helpers proposal 2.1.5.13 */ +function IteratorFind(fn) { + // Step 1. + const iterated = GetIteratorDirectWrapper(this); + + // Step 2. + if (!IsCallable(fn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, fn)); + } + + // Step 3. + for (const value of allowContentIter(iterated)) { + // Steps d-f. + if (callContentFunction(fn, undefined, value)) { + return value; + } + } +} diff --git a/js/src/builtin/JSON.cpp b/js/src/builtin/JSON.cpp new file mode 100644 index 0000000000..7fde8de9b6 --- /dev/null +++ b/js/src/builtin/JSON.cpp @@ -0,0 +1,1383 @@ +/* -*- 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 "builtin/JSON.h" + +#include "mozilla/CheckedInt.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Range.h" +#include "mozilla/ScopeExit.h" + +#include <algorithm> + +#include "jsnum.h" +#include "jstypes.h" + +#include "builtin/Array.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/friend/StackLimits.h" // js::AutoCheckRecursionLimit +#include "js/Object.h" // JS::GetBuiltinClass +#include "js/PropertySpec.h" +#include "js/StableStringChars.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "util/StringBuffer.h" +#include "vm/Interpreter.h" +#include "vm/JSAtom.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/JSONParser.h" +#include "vm/NativeObject.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/WellKnownAtom.h" // js_*_str +#ifdef ENABLE_RECORD_TUPLE +# include "builtin/RecordObject.h" +# include "builtin/TupleObject.h" +# include "vm/RecordType.h" +#endif + +#include "builtin/Array-inl.h" +#include "vm/GeckoProfiler-inl.h" +#include "vm/JSAtom-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::CheckedInt; +using mozilla::Maybe; +using mozilla::RangedPtr; + +using JS::AutoStableStringChars; + +/* ES5 15.12.3 Quote. + * Requires that the destination has enough space allocated for src after + * escaping (that is, `2 + 6 * (srcEnd - srcBegin)` characters). + */ +template <typename SrcCharT, typename DstCharT> +static MOZ_ALWAYS_INLINE RangedPtr<DstCharT> InfallibleQuote( + RangedPtr<const SrcCharT> srcBegin, RangedPtr<const SrcCharT> srcEnd, + RangedPtr<DstCharT> dstPtr) { + // Maps characters < 256 to the value that must follow the '\\' in the quoted + // string. Entries with 'u' are handled as \\u00xy, and entries with 0 are not + // escaped in any way. Characters >= 256 are all assumed to be unescaped. + static const Latin1Char escapeLookup[256] = { + // clang-format off + 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'b', 't', + 'n', 'u', 'f', 'r', 'u', 'u', 'u', 'u', 'u', 'u', + 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', 'u', + 'u', 'u', 0, 0, '\"', 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, '\\', // rest are all zeros + // clang-format on + }; + + /* Step 1. */ + *dstPtr++ = '"'; + + auto ToLowerHex = [](uint8_t u) { + MOZ_ASSERT(u <= 0xF); + return "0123456789abcdef"[u]; + }; + + /* Step 2. */ + while (srcBegin != srcEnd) { + const SrcCharT c = *srcBegin++; + + // Handle the Latin-1 cases. + if (MOZ_LIKELY(c < sizeof(escapeLookup))) { + Latin1Char escaped = escapeLookup[c]; + + // Directly copy non-escaped code points. + if (escaped == 0) { + *dstPtr++ = c; + continue; + } + + // Escape the rest, elaborating Unicode escapes when needed. + *dstPtr++ = '\\'; + *dstPtr++ = escaped; + if (escaped == 'u') { + *dstPtr++ = '0'; + *dstPtr++ = '0'; + + uint8_t x = c >> 4; + MOZ_ASSERT(x < 10); + *dstPtr++ = '0' + x; + + *dstPtr++ = ToLowerHex(c & 0xF); + } + + continue; + } + + // Non-ASCII non-surrogates are directly copied. + if (!unicode::IsSurrogate(c)) { + *dstPtr++ = c; + continue; + } + + // So too for complete surrogate pairs. + if (MOZ_LIKELY(unicode::IsLeadSurrogate(c) && srcBegin < srcEnd && + unicode::IsTrailSurrogate(*srcBegin))) { + *dstPtr++ = c; + *dstPtr++ = *srcBegin++; + continue; + } + + // But lone surrogates are Unicode-escaped. + char32_t as32 = char32_t(c); + *dstPtr++ = '\\'; + *dstPtr++ = 'u'; + *dstPtr++ = ToLowerHex(as32 >> 12); + *dstPtr++ = ToLowerHex((as32 >> 8) & 0xF); + *dstPtr++ = ToLowerHex((as32 >> 4) & 0xF); + *dstPtr++ = ToLowerHex(as32 & 0xF); + } + + /* Steps 3-4. */ + *dstPtr++ = '"'; + return dstPtr; +} + +template <typename SrcCharT, typename DstCharT> +static size_t QuoteHelper(const JSLinearString& linear, StringBuffer& sb, + size_t sbOffset) { + size_t len = linear.length(); + + JS::AutoCheckCannotGC nogc; + RangedPtr<const SrcCharT> srcBegin{linear.chars<SrcCharT>(nogc), len}; + RangedPtr<DstCharT> dstBegin{sb.begin<DstCharT>(), sb.begin<DstCharT>(), + sb.end<DstCharT>()}; + RangedPtr<DstCharT> dstEnd = + InfallibleQuote(srcBegin, srcBegin + len, dstBegin + sbOffset); + + return dstEnd - dstBegin; +} + +static bool Quote(JSContext* cx, StringBuffer& sb, JSString* str) { + JSLinearString* linear = str->ensureLinear(cx); + if (!linear) { + return false; + } + + if (linear->hasTwoByteChars() && !sb.ensureTwoByteChars()) { + return false; + } + + // We resize the backing buffer to the maximum size we could possibly need, + // write the escaped string into it, and shrink it back to the size we ended + // up needing. + + size_t len = linear->length(); + size_t sbInitialLen = sb.length(); + + CheckedInt<size_t> reservedLen = CheckedInt<size_t>(len) * 6 + 2; + if (MOZ_UNLIKELY(!reservedLen.isValid())) { + ReportAllocationOverflow(cx); + return false; + } + + if (!sb.growByUninitialized(reservedLen.value())) { + return false; + } + + size_t newSize; + + if (linear->hasTwoByteChars()) { + newSize = QuoteHelper<char16_t, char16_t>(*linear, sb, sbInitialLen); + } else if (sb.isUnderlyingBufferLatin1()) { + newSize = QuoteHelper<Latin1Char, Latin1Char>(*linear, sb, sbInitialLen); + } else { + newSize = QuoteHelper<Latin1Char, char16_t>(*linear, sb, sbInitialLen); + } + + sb.shrinkTo(newSize); + + return true; +} + +namespace { + +using ObjectVector = GCVector<JSObject*, 8>; + +class StringifyContext { + public: + StringifyContext(JSContext* cx, StringBuffer& sb, const StringBuffer& gap, + HandleObject replacer, const RootedIdVector& propertyList, + bool maybeSafely) + : sb(sb), + gap(gap), + replacer(cx, replacer), + stack(cx, ObjectVector(cx)), + propertyList(propertyList), + depth(0), + maybeSafely(maybeSafely) { + MOZ_ASSERT_IF(maybeSafely, !replacer); + MOZ_ASSERT_IF(maybeSafely, gap.empty()); + } + + StringBuffer& sb; + const StringBuffer& gap; + RootedObject replacer; + Rooted<ObjectVector> stack; + const RootedIdVector& propertyList; + uint32_t depth; + bool maybeSafely; +}; + +} /* anonymous namespace */ + +static bool Str(JSContext* cx, const Value& v, StringifyContext* scx); + +static bool WriteIndent(StringifyContext* scx, uint32_t limit) { + if (!scx->gap.empty()) { + if (!scx->sb.append('\n')) { + return false; + } + + if (scx->gap.isUnderlyingBufferLatin1()) { + for (uint32_t i = 0; i < limit; i++) { + if (!scx->sb.append(scx->gap.rawLatin1Begin(), + scx->gap.rawLatin1End())) { + return false; + } + } + } else { + for (uint32_t i = 0; i < limit; i++) { + if (!scx->sb.append(scx->gap.rawTwoByteBegin(), + scx->gap.rawTwoByteEnd())) { + return false; + } + } + } + } + + return true; +} + +namespace { + +template <typename KeyType> +class KeyStringifier {}; + +template <> +class KeyStringifier<uint32_t> { + public: + static JSString* toString(JSContext* cx, uint32_t index) { + return IndexToString(cx, index); + } +}; + +template <> +class KeyStringifier<HandleId> { + public: + static JSString* toString(JSContext* cx, HandleId id) { + return IdToString(cx, id); + } +}; + +} /* anonymous namespace */ + +/* + * ES5 15.12.3 Str, steps 2-4, extracted to enable preprocessing of property + * values when stringifying objects in JO. + */ +template <typename KeyType> +static bool PreprocessValue(JSContext* cx, HandleObject holder, KeyType key, + MutableHandleValue vp, StringifyContext* scx) { + // We don't want to do any preprocessing here if scx->maybeSafely, + // since the stuff we do here can have side-effects. + if (scx->maybeSafely) { + return true; + } + + RootedString keyStr(cx); + + // Step 2. Modified by BigInt spec 6.1 to check for a toJSON method on the + // BigInt prototype when the value is a BigInt, and to pass the BigInt + // primitive value as receiver. + if (vp.isObject() || vp.isBigInt()) { + RootedValue toJSON(cx); + RootedObject obj(cx, JS::ToObject(cx, vp)); + if (!obj) { + return false; + } + + if (!GetProperty(cx, obj, vp, cx->names().toJSON, &toJSON)) { + return false; + } + + if (IsCallable(toJSON)) { + keyStr = KeyStringifier<KeyType>::toString(cx, key); + if (!keyStr) { + return false; + } + + RootedValue arg0(cx, StringValue(keyStr)); + if (!js::Call(cx, toJSON, vp, arg0, vp)) { + return false; + } + } + } + + /* Step 3. */ + if (scx->replacer && scx->replacer->isCallable()) { + MOZ_ASSERT(holder != nullptr, + "holder object must be present when replacer is callable"); + + if (!keyStr) { + keyStr = KeyStringifier<KeyType>::toString(cx, key); + if (!keyStr) { + return false; + } + } + + RootedValue arg0(cx, StringValue(keyStr)); + RootedValue replacerVal(cx, ObjectValue(*scx->replacer)); + if (!js::Call(cx, replacerVal, holder, arg0, vp, vp)) { + return false; + } + } + + /* Step 4. */ + if (vp.get().isObject()) { + RootedObject obj(cx, &vp.get().toObject()); + + ESClass cls; + if (!JS::GetBuiltinClass(cx, obj, &cls)) { + return false; + } + + if (cls == ESClass::Number) { + double d; + if (!ToNumber(cx, vp, &d)) { + return false; + } + vp.setNumber(d); + } else if (cls == ESClass::String) { + JSString* str = ToStringSlow<CanGC>(cx, vp); + if (!str) { + return false; + } + vp.setString(str); + } else if (cls == ESClass::Boolean || cls == ESClass::BigInt || + IF_RECORD_TUPLE( + obj->is<RecordObject>() || obj->is<TupleObject>(), false)) { + if (!Unbox(cx, obj, vp)) { + return false; + } + } + } + + return true; +} + +/* + * Determines whether a value which has passed by ES5 150.2.3 Str steps 1-4's + * gauntlet will result in Str returning |undefined|. This function is used to + * properly omit properties resulting in such values when stringifying objects, + * while properly stringifying such properties as null when they're encountered + * in arrays. + */ +static inline bool IsFilteredValue(const Value& v) { + return v.isUndefined() || v.isSymbol() || IsCallable(v); +} + +class CycleDetector { + public: + CycleDetector(StringifyContext* scx, HandleObject obj) + : stack_(&scx->stack), obj_(obj), appended_(false) {} + + MOZ_ALWAYS_INLINE bool foundCycle(JSContext* cx) { + JSObject* obj = obj_; + for (JSObject* obj2 : stack_) { + if (MOZ_UNLIKELY(obj == obj2)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_JSON_CYCLIC_VALUE); + return false; + } + } + appended_ = stack_.append(obj); + return appended_; + } + + ~CycleDetector() { + if (MOZ_LIKELY(appended_)) { + MOZ_ASSERT(stack_.back() == obj_); + stack_.popBack(); + } + } + + private: + MutableHandle<ObjectVector> stack_; + HandleObject obj_; + bool appended_; +}; + +#ifdef ENABLE_RECORD_TUPLE +enum class JOType { Record, Object }; +template <JOType type = JOType::Object> +#endif +/* ES5 15.12.3 JO. */ +static bool JO(JSContext* cx, HandleObject obj, StringifyContext* scx) { + /* + * This method implements the JO algorithm in ES5 15.12.3, but: + * + * * The algorithm is somewhat reformulated to allow the final string to + * be streamed into a single buffer, rather than be created and copied + * into place incrementally as the ES5 algorithm specifies it. This + * requires moving portions of the Str call in 8a into this algorithm + * (and in JA as well). + */ + +#ifdef ENABLE_RECORD_TUPLE + RecordType* rec; + + if constexpr (type == JOType::Record) { + MOZ_ASSERT(obj->is<RecordType>()); + rec = &obj->as<RecordType>(); + } else { + MOZ_ASSERT(!IsExtendedPrimitive(*obj)); + } +#endif + MOZ_ASSERT_IF(scx->maybeSafely, obj->is<PlainObject>()); + + /* Steps 1-2, 11. */ + CycleDetector detect(scx, obj); + if (!detect.foundCycle(cx)) { + return false; + } + + if (!scx->sb.append('{')) { + return false; + } + + /* Steps 5-7. */ + Maybe<RootedIdVector> ids; + const RootedIdVector* props; + if (scx->replacer && !scx->replacer->isCallable()) { + // NOTE: We can't assert |IsArray(scx->replacer)| because the replacer + // might have been a revocable proxy to an array. Such a proxy + // satisfies |IsArray|, but any side effect of JSON.stringify + // could revoke the proxy so that |!IsArray(scx->replacer)|. See + // bug 1196497. + props = &scx->propertyList; + } else { + MOZ_ASSERT_IF(scx->replacer, scx->propertyList.length() == 0); + ids.emplace(cx); + if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, ids.ptr())) { + return false; + } + props = ids.ptr(); + } + + /* My kingdom for not-quite-initialized-from-the-start references. */ + const RootedIdVector& propertyList = *props; + + /* Steps 8-10, 13. */ + bool wroteMember = false; + RootedId id(cx); + for (size_t i = 0, len = propertyList.length(); i < len; i++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + /* + * Steps 8a-8b. Note that the call to Str is broken up into 1) getting + * the property; 2) processing for toJSON, calling the replacer, and + * handling boxed Number/String/Boolean objects; 3) filtering out + * values which process to |undefined|, and 4) stringifying all values + * which pass the filter. + */ + id = propertyList[i]; + RootedValue outputValue(cx); +#ifdef DEBUG + if (scx->maybeSafely) { + PropertyResult prop; + if (!NativeLookupOwnPropertyNoResolve(cx, &obj->as<NativeObject>(), id, + &prop)) { + return false; + } + MOZ_ASSERT(prop.isNativeProperty() && + prop.propertyInfo().isDataDescriptor()); + } +#endif // DEBUG + +#ifdef ENABLE_RECORD_TUPLE + if constexpr (type == JOType::Record) { + MOZ_ALWAYS_TRUE(rec->getOwnProperty(cx, id, &outputValue)); + } else +#endif + { + RootedValue objValue(cx, ObjectValue(*obj)); + if (!GetProperty(cx, obj, objValue, id, &outputValue)) { + return false; + } + } + if (!PreprocessValue(cx, obj, HandleId(id), &outputValue, scx)) { + return false; + } + if (IsFilteredValue(outputValue)) { + continue; + } + + /* Output a comma unless this is the first member to write. */ + if (wroteMember && !scx->sb.append(',')) { + return false; + } + wroteMember = true; + + if (!WriteIndent(scx, scx->depth)) { + return false; + } + + JSString* s = IdToString(cx, id); + if (!s) { + return false; + } + + if (!Quote(cx, scx->sb, s) || !scx->sb.append(':') || + !(scx->gap.empty() || scx->sb.append(' ')) || + !Str(cx, outputValue, scx)) { + return false; + } + } + + if (wroteMember && !WriteIndent(scx, scx->depth - 1)) { + return false; + } + + return scx->sb.append('}'); +} + +// For JSON.stringify and JSON.parse with a reviver function, we need to know +// the length of an object for which JS::IsArray returned true. This must be +// either an ArrayObject or a proxy wrapping one. +static MOZ_ALWAYS_INLINE bool GetLengthPropertyForArrayLike(JSContext* cx, + HandleObject obj, + uint32_t* lengthp) { + if (MOZ_LIKELY(obj->is<ArrayObject>())) { + *lengthp = obj->as<ArrayObject>().length(); + return true; + } +#ifdef ENABLE_RECORD_TUPLE + if (obj->is<TupleType>()) { + *lengthp = obj->as<TupleType>().length(); + return true; + } +#endif + + MOZ_ASSERT(obj->is<ProxyObject>()); + + uint64_t len = 0; + if (!GetLengthProperty(cx, obj, &len)) { + return false; + } + + // A scripted proxy wrapping an array can return a length value larger than + // UINT32_MAX. Stringification will likely report an OOM in this case. Match + // other JS engines and report an early error in this case, although + // technically this is observable, for example when stringifying with a + // replacer function. + if (len > UINT32_MAX) { + ReportAllocationOverflow(cx); + return false; + } + + *lengthp = uint32_t(len); + return true; +} + +/* ES5 15.12.3 JA. */ +static bool JA(JSContext* cx, HandleObject obj, StringifyContext* scx) { + /* + * This method implements the JA algorithm in ES5 15.12.3, but: + * + * * The algorithm is somewhat reformulated to allow the final string to + * be streamed into a single buffer, rather than be created and copied + * into place incrementally as the ES5 algorithm specifies it. This + * requires moving portions of the Str call in 8a into this algorithm + * (and in JO as well). + */ + + /* Steps 1-2, 11. */ + CycleDetector detect(scx, obj); + if (!detect.foundCycle(cx)) { + return false; + } + + if (!scx->sb.append('[')) { + return false; + } + + /* Step 6. */ + uint32_t length; + if (!GetLengthPropertyForArrayLike(cx, obj, &length)) { + return false; + } + + /* Steps 7-10. */ + if (length != 0) { + /* Steps 4, 10b(i). */ + if (!WriteIndent(scx, scx->depth)) { + return false; + } + + /* Steps 7-10. */ + RootedValue outputValue(cx); + for (uint32_t i = 0; i < length; i++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + /* + * Steps 8a-8c. Again note how the call to the spec's Str method + * is broken up into getting the property, running it past toJSON + * and the replacer and maybe unboxing, and interpreting some + * values as |null| in separate steps. + */ +#ifdef DEBUG + if (scx->maybeSafely) { + /* + * Trying to do a JS_AlreadyHasOwnElement runs the risk of + * hitting OOM on jsid creation. Let's just assert sanity for + * small enough indices. + */ + MOZ_ASSERT(obj->is<ArrayObject>()); + MOZ_ASSERT(obj->is<NativeObject>()); + Rooted<NativeObject*> nativeObj(cx, &obj->as<NativeObject>()); + if (i <= PropertyKey::IntMax) { + MOZ_ASSERT( + nativeObj->containsDenseElement(i) != nativeObj->isIndexed(), + "the array must either be small enough to remain " + "fully dense (and otherwise un-indexed), *or* " + "all its initially-dense elements were sparsified " + "and the object is indexed"); + } else { + MOZ_ASSERT(nativeObj->isIndexed()); + } + } +#endif + if (!GetElement(cx, obj, i, &outputValue)) { + return false; + } + if (!PreprocessValue(cx, obj, i, &outputValue, scx)) { + return false; + } + if (IsFilteredValue(outputValue)) { + if (!scx->sb.append("null")) { + return false; + } + } else { + if (!Str(cx, outputValue, scx)) { + return false; + } + } + + /* Steps 3, 4, 10b(i). */ + if (i < length - 1) { + if (!scx->sb.append(',')) { + return false; + } + if (!WriteIndent(scx, scx->depth)) { + return false; + } + } + } + + /* Step 10(b)(iii). */ + if (!WriteIndent(scx, scx->depth - 1)) { + return false; + } + } + + return scx->sb.append(']'); +} + +static bool Str(JSContext* cx, const Value& v, StringifyContext* scx) { + /* Step 11 must be handled by the caller. */ + MOZ_ASSERT(!IsFilteredValue(v)); + + /* + * This method implements the Str algorithm in ES5 15.12.3, but: + * + * * We move property retrieval (step 1) into callers to stream the + * stringification process and avoid constantly copying strings. + * * We move the preprocessing in steps 2-4 into a helper function to + * allow both JO and JA to use this method. While JA could use it + * without this move, JO must omit any |undefined|-valued property per + * so it can't stream out a value using the Str method exactly as + * defined by ES5. + * * We move step 11 into callers, again to ease streaming. + */ + + /* Step 8. */ + if (v.isString()) { + return Quote(cx, scx->sb, v.toString()); + } + + /* Step 5. */ + if (v.isNull()) { + return scx->sb.append("null"); + } + + /* Steps 6-7. */ + if (v.isBoolean()) { + return v.toBoolean() ? scx->sb.append("true") : scx->sb.append("false"); + } + + /* Step 9. */ + if (v.isNumber()) { + if (v.isDouble()) { + if (!std::isfinite(v.toDouble())) { + MOZ_ASSERT(!scx->maybeSafely, + "input JS::ToJSONMaybeSafely must not include " + "reachable non-finite numbers"); + return scx->sb.append("null"); + } + } + + return NumberValueToStringBuffer(v, scx->sb); + } + + /* Step 10 in the BigInt proposal. */ + if (v.isBigInt()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BIGINT_NOT_SERIALIZABLE); + return false; + } + + AutoCheckRecursionLimit recursion(cx); + if (!recursion.check(cx)) { + return false; + } + + /* Step 10. */ + MOZ_ASSERT(v.hasObjectPayload()); + RootedObject obj(cx, &v.getObjectPayload()); + + MOZ_ASSERT( + !scx->maybeSafely || obj->is<PlainObject>() || obj->is<ArrayObject>(), + "input to JS::ToJSONMaybeSafely must not include reachable " + "objects that are neither arrays nor plain objects"); + + scx->depth++; + auto dec = mozilla::MakeScopeExit([&] { scx->depth--; }); + +#ifdef ENABLE_RECORD_TUPLE + if (v.isExtendedPrimitive()) { + if (obj->is<RecordType>()) { + return JO<JOType::Record>(cx, obj, scx); + } + if (obj->is<TupleType>()) { + return JA(cx, obj, scx); + } + MOZ_CRASH("Unexpected extended primitive - boxes cannot be stringified."); + } +#endif + + bool isArray; + if (!IsArray(cx, obj, &isArray)) { + return false; + } + + return isArray ? JA(cx, obj, scx) : JO(cx, obj, scx); +} + +/* ES6 24.3.2. */ +bool js::Stringify(JSContext* cx, MutableHandleValue vp, JSObject* replacer_, + const Value& space_, StringBuffer& sb, + StringifyBehavior stringifyBehavior) { + RootedObject replacer(cx, replacer_); + RootedValue space(cx, space_); + + MOZ_ASSERT_IF(stringifyBehavior == StringifyBehavior::RestrictedSafe, + space.isNull()); + MOZ_ASSERT_IF(stringifyBehavior == StringifyBehavior::RestrictedSafe, + vp.isObject()); + /** + * This uses MOZ_ASSERT, since it's actually asserting something jsapi + * consumers could get wrong, so needs a better error message. + */ + MOZ_ASSERT(stringifyBehavior == StringifyBehavior::Normal || + vp.toObject().is<PlainObject>() || + vp.toObject().is<ArrayObject>(), + "input to JS::ToJSONMaybeSafely must be a plain object or array"); + + /* Step 4. */ + RootedIdVector propertyList(cx); + if (replacer) { + bool isArray; + if (replacer->isCallable()) { + /* Step 4a(i): use replacer to transform values. */ + } else if (!IsArray(cx, replacer, &isArray)) { + return false; + } else if (isArray) { + /* Step 4b(iii). */ + + /* Step 4b(iii)(2-3). */ + uint32_t len; + if (!GetLengthPropertyForArrayLike(cx, replacer, &len)) { + return false; + } + + // Cap the initial size to a moderately small value. This avoids + // ridiculous over-allocation if an array with bogusly-huge length + // is passed in. If we end up having to add elements past this + // size, the set will naturally resize to accommodate them. + const uint32_t MaxInitialSize = 32; + Rooted<GCHashSet<jsid>> idSet( + cx, GCHashSet<jsid>(cx, std::min(len, MaxInitialSize))); + + /* Step 4b(iii)(4). */ + uint32_t k = 0; + + /* Step 4b(iii)(5). */ + RootedValue item(cx); + for (; k < len; k++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + /* Step 4b(iii)(5)(a-b). */ + if (!GetElement(cx, replacer, k, &item)) { + return false; + } + + /* Step 4b(iii)(5)(c-g). */ + RootedId id(cx); + if (item.isNumber() || item.isString()) { + if (!PrimitiveValueToId<CanGC>(cx, item, &id)) { + return false; + } + } else { + ESClass cls; + if (!GetClassOfValue(cx, item, &cls)) { + return false; + } + + if (cls != ESClass::String && cls != ESClass::Number) { + continue; + } + + JSAtom* atom = ToAtom<CanGC>(cx, item); + if (!atom) { + return false; + } + + id.set(AtomToId(atom)); + } + + /* Step 4b(iii)(5)(g). */ + auto p = idSet.lookupForAdd(id); + if (!p) { + /* Step 4b(iii)(5)(g)(i). */ + if (!idSet.add(p, id) || !propertyList.append(id)) { + return false; + } + } + } + } else { + replacer = nullptr; + } + } + + /* Step 5. */ + if (space.isObject()) { + RootedObject spaceObj(cx, &space.toObject()); + + ESClass cls; + if (!JS::GetBuiltinClass(cx, spaceObj, &cls)) { + return false; + } + + if (cls == ESClass::Number) { + double d; + if (!ToNumber(cx, space, &d)) { + return false; + } + space = NumberValue(d); + } else if (cls == ESClass::String) { + JSString* str = ToStringSlow<CanGC>(cx, space); + if (!str) { + return false; + } + space = StringValue(str); + } + } + + StringBuffer gap(cx); + + if (space.isNumber()) { + /* Step 6. */ + double d; + MOZ_ALWAYS_TRUE(ToInteger(cx, space, &d)); + d = std::min(10.0, d); + if (d >= 1 && !gap.appendN(' ', uint32_t(d))) { + return false; + } + } else if (space.isString()) { + /* Step 7. */ + JSLinearString* str = space.toString()->ensureLinear(cx); + if (!str) { + return false; + } + size_t len = std::min(size_t(10), str->length()); + if (!gap.appendSubstring(str, 0, len)) { + return false; + } + } else { + /* Step 8. */ + MOZ_ASSERT(gap.empty()); + } + + Rooted<PlainObject*> wrapper(cx); + RootedId emptyId(cx, NameToId(cx->names().empty)); + if (replacer && replacer->isCallable()) { + // We can skip creating the initial wrapper object if no replacer + // function is present. + + /* Step 9. */ + wrapper = NewPlainObject(cx); + if (!wrapper) { + return false; + } + + /* Steps 10-11. */ + if (!NativeDefineDataProperty(cx, wrapper, emptyId, vp, JSPROP_ENUMERATE)) { + return false; + } + } + + /* Step 12. */ + StringifyContext scx(cx, sb, gap, replacer, propertyList, + stringifyBehavior == StringifyBehavior::RestrictedSafe); + if (!PreprocessValue(cx, wrapper, HandleId(emptyId), vp, &scx)) { + return false; + } + if (IsFilteredValue(vp)) { + return true; + } + + return Str(cx, vp, &scx); +} + +/* ES5 15.12.2 Walk. */ +static bool Walk(JSContext* cx, HandleObject holder, HandleId name, + HandleValue reviver, MutableHandleValue vp) { + AutoCheckRecursionLimit recursion(cx); + if (!recursion.check(cx)) { + return false; + } + + /* Step 1. */ + RootedValue val(cx); + if (!GetProperty(cx, holder, holder, name, &val)) { + return false; + } + + /* Step 2. */ + if (val.isObject()) { + RootedObject obj(cx, &val.toObject()); + + bool isArray; + if (!IsArray(cx, obj, &isArray)) { + return false; + } + + if (isArray) { + /* Step 2a(ii). */ + uint32_t length; + if (!GetLengthPropertyForArrayLike(cx, obj, &length)) { + return false; + } + + /* Step 2a(i), 2a(iii-iv). */ + RootedId id(cx); + RootedValue newElement(cx); + for (uint32_t i = 0; i < length; i++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + if (!IndexToId(cx, i, &id)) { + return false; + } + + /* Step 2a(iii)(1). */ + if (!Walk(cx, obj, id, reviver, &newElement)) { + return false; + } + + ObjectOpResult ignored; + if (newElement.isUndefined()) { + /* Step 2a(iii)(2). The spec deliberately ignores strict failure. */ + if (!DeleteProperty(cx, obj, id, ignored)) { + return false; + } + } else { + /* Step 2a(iii)(3). The spec deliberately ignores strict failure. */ + Rooted<PropertyDescriptor> desc( + cx, PropertyDescriptor::Data(newElement, + {JS::PropertyAttribute::Configurable, + JS::PropertyAttribute::Enumerable, + JS::PropertyAttribute::Writable})); + if (!DefineProperty(cx, obj, id, desc, ignored)) { + return false; + } + } + } + } else { + /* Step 2b(i). */ + RootedIdVector keys(cx); + if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &keys)) { + return false; + } + + /* Step 2b(ii). */ + RootedId id(cx); + RootedValue newElement(cx); + for (size_t i = 0, len = keys.length(); i < len; i++) { + if (!CheckForInterrupt(cx)) { + return false; + } + + /* Step 2b(ii)(1). */ + id = keys[i]; + if (!Walk(cx, obj, id, reviver, &newElement)) { + return false; + } + + ObjectOpResult ignored; + if (newElement.isUndefined()) { + /* Step 2b(ii)(2). The spec deliberately ignores strict failure. */ + if (!DeleteProperty(cx, obj, id, ignored)) { + return false; + } + } else { + /* Step 2b(ii)(3). The spec deliberately ignores strict failure. */ + Rooted<PropertyDescriptor> desc( + cx, PropertyDescriptor::Data(newElement, + {JS::PropertyAttribute::Configurable, + JS::PropertyAttribute::Enumerable, + JS::PropertyAttribute::Writable})); + if (!DefineProperty(cx, obj, id, desc, ignored)) { + return false; + } + } + } + } + } + + /* Step 3. */ + RootedString key(cx, IdToString(cx, name)); + if (!key) { + return false; + } + + RootedValue keyVal(cx, StringValue(key)); + return js::Call(cx, reviver, holder, keyVal, val, vp); +} + +static bool Revive(JSContext* cx, HandleValue reviver, MutableHandleValue vp) { + Rooted<PlainObject*> obj(cx, NewPlainObject(cx)); + if (!obj) { + return false; + } + + if (!DefineDataProperty(cx, obj, cx->names().empty, vp)) { + return false; + } + + Rooted<jsid> id(cx, NameToId(cx->names().empty)); + return Walk(cx, obj, id, reviver, vp); +} + +template <typename CharT> +bool ParseJSON(JSContext* cx, const mozilla::Range<const CharT> chars, + MutableHandleValue vp) { + Rooted<JSONParser<CharT>> parser( + cx, + JSONParser<CharT>(cx, chars, JSONParser<CharT>::ParseType::JSONParse)); + return parser.parse(vp); +} + +template <typename CharT> +bool js::ParseJSONWithReviver(JSContext* cx, + const mozilla::Range<const CharT> chars, + HandleValue reviver, MutableHandleValue vp) { + /* 15.12.2 steps 2-3. */ + if (!ParseJSON(cx, chars, vp)) { + return false; + } + + /* 15.12.2 steps 4-5. */ + if (IsCallable(reviver)) { + return Revive(cx, reviver, vp); + } + return true; +} + +template bool js::ParseJSONWithReviver( + JSContext* cx, const mozilla::Range<const Latin1Char> chars, + HandleValue reviver, MutableHandleValue vp); + +template bool js::ParseJSONWithReviver( + JSContext* cx, const mozilla::Range<const char16_t> chars, + HandleValue reviver, MutableHandleValue vp); + +static bool json_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().JSON); + return true; +} + +/* ES5 15.12.2. */ +static bool json_parse(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "parse"); + CallArgs args = CallArgsFromVp(argc, vp); + + /* Step 1. */ + JSString* str = (args.length() >= 1) ? ToString<CanGC>(cx, args[0]) + : cx->names().undefined; + if (!str) { + return false; + } + + JSLinearString* linear = str->ensureLinear(cx); + if (!linear) { + return false; + } + + AutoStableStringChars linearChars(cx); + if (!linearChars.init(cx, linear)) { + return false; + } + + HandleValue reviver = args.get(1); + + /* Steps 2-5. */ + return linearChars.isLatin1() + ? ParseJSONWithReviver(cx, linearChars.latin1Range(), reviver, + args.rval()) + : ParseJSONWithReviver(cx, linearChars.twoByteRange(), reviver, + args.rval()); +} + +#ifdef ENABLE_RECORD_TUPLE +bool BuildImmutableProperty(JSContext* cx, HandleValue value, HandleId name, + HandleValue reviver, + MutableHandleValue immutableRes) { + MOZ_ASSERT(!name.isSymbol()); + + // Step 1 + if (value.isObject()) { + RootedValue childValue(cx), newElement(cx); + RootedId childName(cx); + + // Step 1.a-1.b + if (value.toObject().is<ArrayObject>()) { + Rooted<ArrayObject*> arr(cx, &value.toObject().as<ArrayObject>()); + + // Step 1.b.iii + uint32_t len = arr->length(); + + TupleType* tup = TupleType::createUninitialized(cx, len); + if (!tup) { + return false; + } + immutableRes.setExtendedPrimitive(*tup); + + // Step 1.b.iv + for (uint32_t i = 0; i < len; i++) { + // Step 1.b.iv.1 + childName.set(PropertyKey::Int(i)); + + // Step 1.b.iv.2 + if (!GetProperty(cx, arr, value, childName, &childValue)) { + return false; + } + + // Step 1.b.iv.3 + if (!BuildImmutableProperty(cx, childValue, childName, reviver, + &newElement)) { + return false; + } + MOZ_ASSERT(newElement.isPrimitive()); + + // Step 1.b.iv.5 + if (!tup->initializeNextElement(cx, newElement)) { + return false; + } + } + + // Step 1.b.v + tup->finishInitialization(cx); + } else { + RootedObject obj(cx, &value.toObject()); + + // Step 1.c.i - We only get the property keys rather than the + // entries, but the difference is not observable from user code + // because `obj` is a plan object not exposed externally + RootedIdVector props(cx); + if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &props)) { + return false; + } + + RecordType* rec = RecordType::createUninitialized(cx, props.length()); + if (!rec) { + return false; + } + immutableRes.setExtendedPrimitive(*rec); + + for (uint32_t i = 0; i < props.length(); i++) { + // Step 1.c.iii.1 + childName.set(props[i]); + + // Step 1.c.iii.2 + if (!GetProperty(cx, obj, value, childName, &childValue)) { + return false; + } + + // Step 1.c.iii.3 + if (!BuildImmutableProperty(cx, childValue, childName, reviver, + &newElement)) { + return false; + } + MOZ_ASSERT(newElement.isPrimitive()); + + // Step 1.c.iii.5 + if (!newElement.isUndefined()) { + // Step 1.c.iii.5.a-b + rec->initializeNextProperty(cx, childName, newElement); + } + } + + // Step 1.c.iv + rec->finishInitialization(cx); + } + } else { + // Step 2.a + immutableRes.set(value); + } + + // Step 3 + if (IsCallable(reviver)) { + RootedValue keyVal(cx, StringValue(IdToString(cx, name))); + + // Step 3.a + if (!Call(cx, reviver, UndefinedHandleValue, keyVal, immutableRes, + immutableRes)) { + return false; + } + + // Step 3.b + if (!immutableRes.isPrimitive()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_RECORD_TUPLE_NO_OBJECT); + return false; + } + } + + return true; +} + +static bool json_parseImmutable(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "parseImmutable"); + CallArgs args = CallArgsFromVp(argc, vp); + + /* Step 1. */ + JSString* str = (args.length() >= 1) ? ToString<CanGC>(cx, args[0]) + : cx->names().undefined; + if (!str) { + return false; + } + + JSLinearString* linear = str->ensureLinear(cx); + if (!linear) { + return false; + } + + AutoStableStringChars linearChars(cx); + if (!linearChars.init(cx, linear)) { + return false; + } + + HandleValue reviver = args.get(1); + RootedValue unfiltered(cx); + + if (linearChars.isLatin1()) { + if (!ParseJSON(cx, linearChars.latin1Range(), &unfiltered)) { + return false; + } + } else { + if (!ParseJSON(cx, linearChars.twoByteRange(), &unfiltered)) { + return false; + } + } + + RootedId id(cx, NameToId(cx->names().empty)); + return BuildImmutableProperty(cx, unfiltered, id, reviver, args.rval()); +} +#endif + +/* ES6 24.3.2. */ +bool json_stringify(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "JSON", "stringify"); + CallArgs args = CallArgsFromVp(argc, vp); + + RootedObject replacer(cx, + args.get(1).isObject() ? &args[1].toObject() : nullptr); + RootedValue value(cx, args.get(0)); + RootedValue space(cx, args.get(2)); + + JSStringBuilder sb(cx); + if (!Stringify(cx, &value, replacer, space, sb, StringifyBehavior::Normal)) { + return false; + } + + // XXX This can never happen to nsJSON.cpp, but the JSON object + // needs to support returning undefined. So this is a little awkward + // for the API, because we want to support streaming writers. + if (!sb.empty()) { + JSString* str = sb.finishString(); + if (!str) { + return false; + } + args.rval().setString(str); + } else { + args.rval().setUndefined(); + } + + return true; +} + +static const JSFunctionSpec json_static_methods[] = { + JS_FN(js_toSource_str, json_toSource, 0, 0), + JS_FN("parse", json_parse, 2, 0), JS_FN("stringify", json_stringify, 3, 0), +#ifdef ENABLE_RECORD_TUPLE + JS_FN("parseImmutable", json_parseImmutable, 2, 0), +#endif + JS_FS_END}; + +static const JSPropertySpec json_static_properties[] = { + JS_STRING_SYM_PS(toStringTag, "JSON", JSPROP_READONLY), JS_PS_END}; + +static JSObject* CreateJSONObject(JSContext* cx, JSProtoKey key) { + RootedObject proto(cx, &cx->global()->getObjectPrototype()); + return NewTenuredObjectWithGivenProto(cx, &JSONClass, proto); +} + +static const ClassSpec JSONClassSpec = { + CreateJSONObject, nullptr, json_static_methods, json_static_properties}; + +const JSClass js::JSONClass = {js_JSON_str, + JSCLASS_HAS_CACHED_PROTO(JSProto_JSON), + JS_NULL_CLASS_OPS, &JSONClassSpec}; diff --git a/js/src/builtin/JSON.h b/js/src/builtin/JSON.h new file mode 100644 index 0000000000..7bf4342c97 --- /dev/null +++ b/js/src/builtin/JSON.h @@ -0,0 +1,40 @@ +/* -*- 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 builtin_JSON_h +#define builtin_JSON_h + +#include "mozilla/Range.h" + +#include "NamespaceImports.h" + +#include "js/RootingAPI.h" + +namespace js { + +class StringBuffer; + +extern const JSClass JSONClass; + +enum class StringifyBehavior { Normal, RestrictedSafe }; + +/** + * If maybeSafely is true, Stringify will attempt to assert the API requirements + * of JS::ToJSONMaybeSafely as it traverses the graph, and will not try to + * invoke .toJSON on things as it goes. + */ +extern bool Stringify(JSContext* cx, js::MutableHandleValue vp, + JSObject* replacer, const Value& space, StringBuffer& sb, + StringifyBehavior stringifyBehavior); + +template <typename CharT> +extern bool ParseJSONWithReviver(JSContext* cx, + const mozilla::Range<const CharT> chars, + HandleValue reviver, MutableHandleValue vp); + +} // namespace js + +#endif /* builtin_JSON_h */ diff --git a/js/src/builtin/Map.js b/js/src/builtin/Map.js new file mode 100644 index 0000000000..a7826ab8aa --- /dev/null +++ b/js/src/builtin/Map.js @@ -0,0 +1,133 @@ +/* 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/. */ + +// ES2017 draft rev 0e10c9f29fca1385980c08a7d5e7bb3eb775e2e4 +// 23.1.1.1 Map, steps 6-8 +function MapConstructorInit(iterable) { + var map = this; + + // Step 6.a. + var adder = map.set; + + // Step 6.b. + if (!IsCallable(adder)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, typeof adder); + } + + // Steps 6.c-8. + for (var nextItem of allowContentIter(iterable)) { + // Step 8.d. + if (!IsObject(nextItem)) { + ThrowTypeError(JSMSG_INVALID_MAP_ITERABLE, "Map"); + } + + // Steps 8.e-j. + callContentFunction(adder, map, nextItem[0], nextItem[1]); + } +} + +// ES2018 draft rev f83aa38282c2a60c6916ebc410bfdf105a0f6a54 +// 23.1.3.5 Map.prototype.forEach ( callbackfn [ , thisArg ] ) +function MapForEach(callbackfn, thisArg = undefined) { + // Step 1. + var M = this; + + // Steps 2-3. + if (!IsObject(M) || (M = GuardToMapObject(M)) === null) { + return callFunction( + CallMapMethodIfWrapped, + this, + callbackfn, + thisArg, + "MapForEach" + ); + } + + // Step 4. + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + // Steps 5-8. + var entries = callFunction(std_Map_entries, M); + + // Inlined: MapIteratorNext + var mapIterationResultPair = globalMapIterationResultPair; + + while (true) { + var done = GetNextMapEntryForIterator(entries, mapIterationResultPair); + if (done) { + break; + } + + var key = mapIterationResultPair[0]; + var value = mapIterationResultPair[1]; + mapIterationResultPair[0] = null; + mapIterationResultPair[1] = null; + + callContentFunction(callbackfn, thisArg, value, key, M); + } +} + +var globalMapIterationResultPair = CreateMapIterationResultPair(); + +function MapIteratorNext() { + // Step 1. + var O = this; + + // Steps 2-3. + if (!IsObject(O) || (O = GuardToMapIterator(O)) === null) { + return callFunction( + CallMapIteratorMethodIfWrapped, + this, + "MapIteratorNext" + ); + } + + // Steps 4-5 (implemented in GetNextMapEntryForIterator). + // Steps 8-9 (omitted). + + var mapIterationResultPair = globalMapIterationResultPair; + + var retVal = { value: undefined, done: true }; + + // Step 10.a, 11. + var done = GetNextMapEntryForIterator(O, mapIterationResultPair); + if (!done) { + // Steps 10.b-c (omitted). + + // Step 6. + var itemKind = UnsafeGetInt32FromReservedSlot(O, ITERATOR_SLOT_ITEM_KIND); + + var result; + if (itemKind === ITEM_KIND_KEY) { + // Step 10.d.i. + result = mapIterationResultPair[0]; + } else if (itemKind === ITEM_KIND_VALUE) { + // Step 10.d.ii. + result = mapIterationResultPair[1]; + } else { + // Step 10.d.iii. + assert(itemKind === ITEM_KIND_KEY_AND_VALUE, itemKind); + result = [mapIterationResultPair[0], mapIterationResultPair[1]]; + } + + mapIterationResultPair[0] = null; + mapIterationResultPair[1] = null; + retVal.value = result; + retVal.done = false; + } + + // Steps 7, 12. + return retVal; +} + +// ES6 final draft 23.1.2.2. +// Uncloned functions with `$` prefix are allocated as extended function +// to store the original name in `SetCanonicalName`. +function $MapSpecies() { + // Step 1. + return this; +} +SetCanonicalName($MapSpecies, "get [Symbol.species]"); diff --git a/js/src/builtin/MapObject.cpp b/js/src/builtin/MapObject.cpp new file mode 100644 index 0000000000..8698fcec1a --- /dev/null +++ b/js/src/builtin/MapObject.cpp @@ -0,0 +1,1970 @@ +/* -*- 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 "builtin/MapObject.h" + +#include "jsapi.h" + +#include "ds/OrderedHashTable.h" +#include "gc/GCContext.h" +#include "jit/InlinableNatives.h" +#include "js/MapAndSet.h" +#include "js/PropertyAndElement.h" // JS_DefineFunctions +#include "js/PropertySpec.h" +#include "js/Utility.h" +#include "vm/BigIntType.h" +#include "vm/EqualityOperations.h" // js::SameValue +#include "vm/GlobalObject.h" +#include "vm/Interpreter.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/SelfHosting.h" +#include "vm/SymbolType.h" + +#ifdef ENABLE_RECORD_TUPLE +# include "vm/RecordType.h" +# include "vm/TupleType.h" +#endif + +#include "gc/GCContext-inl.h" +#include "gc/Marking-inl.h" +#include "vm/GeckoProfiler-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::NumberEqualsInt32; + +/*** HashableValue **********************************************************/ + +static PreBarriered<Value> NormalizeDoubleValue(double d) { + int32_t i; + if (NumberEqualsInt32(d, &i)) { + // Normalize int32_t-valued doubles to int32_t for faster hashing and + // testing. Note: we use NumberEqualsInt32 here instead of NumberIsInt32 + // because we want -0 and 0 to be normalized to the same thing. + return Int32Value(i); + } + + // Normalize the sign bit of a NaN. + return JS::CanonicalizedDoubleValue(d); +} + +bool HashableValue::setValue(JSContext* cx, HandleValue v) { + if (v.isString()) { + // Atomize so that hash() and operator==() are fast and infallible. + JSString* str = AtomizeString(cx, v.toString()); + if (!str) { + return false; + } + value = StringValue(str); + } else if (v.isDouble()) { + value = NormalizeDoubleValue(v.toDouble()); +#ifdef ENABLE_RECORD_TUPLE + } else if (v.isExtendedPrimitive()) { + JSObject& obj = v.toExtendedPrimitive(); + if (obj.is<RecordType>()) { + if (!obj.as<RecordType>().ensureAtomized(cx)) { + return false; + } + } else { + MOZ_ASSERT(obj.is<TupleType>()); + if (!obj.as<TupleType>().ensureAtomized(cx)) { + return false; + } + } + value = v; +#endif + } else { + value = v; + } + + MOZ_ASSERT(value.isUndefined() || value.isNull() || value.isBoolean() || + value.isNumber() || value.isString() || value.isSymbol() || + value.isObject() || value.isBigInt() || + IF_RECORD_TUPLE(value.isExtendedPrimitive(), false)); + return true; +} + +static HashNumber HashValue(const Value& v, + const mozilla::HashCodeScrambler& hcs) { + // HashableValue::setValue normalizes values so that the SameValue relation + // on HashableValues is the same as the == relationship on + // value.asRawBits(). So why not just return that? Security. + // + // To avoid revealing GC of atoms, string-based hash codes are computed + // from the string contents rather than any pointer; to avoid revealing + // addresses, pointer-based hash codes are computed using the + // HashCodeScrambler. + + if (v.isString()) { + return v.toString()->asAtom().hash(); + } + if (v.isSymbol()) { + return v.toSymbol()->hash(); + } + if (v.isBigInt()) { + return MaybeForwarded(v.toBigInt())->hash(); + } +#ifdef ENABLE_RECORD_TUPLE + if (v.isExtendedPrimitive()) { + JSObject* obj = MaybeForwarded(&v.toExtendedPrimitive()); + auto hasher = [&hcs](const Value& v) { + return HashValue( + v.isDouble() ? NormalizeDoubleValue(v.toDouble()).get() : v, hcs); + }; + + if (obj->is<RecordType>()) { + return obj->as<RecordType>().hash(hasher); + } + MOZ_ASSERT(obj->is<TupleType>()); + return obj->as<TupleType>().hash(hasher); + } +#endif + if (v.isObject()) { + return hcs.scramble(v.asRawBits()); + } + + MOZ_ASSERT(!v.isGCThing(), "do not reveal pointers via hash codes"); + return mozilla::HashGeneric(v.asRawBits()); +} + +HashNumber HashableValue::hash(const mozilla::HashCodeScrambler& hcs) const { + return HashValue(value, hcs); +} + +#ifdef ENABLE_RECORD_TUPLE +inline bool SameExtendedPrimitiveType(const PreBarriered<Value>& a, + const PreBarriered<Value>& b) { + return a.toExtendedPrimitive().getClass() == + b.toExtendedPrimitive().getClass(); +} +#endif + +bool HashableValue::equals(const HashableValue& other) const { + // Two HashableValues are equal if they have equal bits. + bool b = (value.asRawBits() == other.value.asRawBits()); + + if (!b && (value.type() == other.value.type())) { + if (value.isBigInt()) { + // BigInt values are considered equal if they represent the same + // mathematical value. + b = BigInt::equal(value.toBigInt(), other.value.toBigInt()); + } +#ifdef ENABLE_RECORD_TUPLE + else if (value.isExtendedPrimitive() && + SameExtendedPrimitiveType(value, other.value)) { + b = js::SameValueZeroLinear(value, other.value); + } +#endif + } + +#ifdef DEBUG + bool same; + JSContext* cx = TlsContext.get(); + RootedValue valueRoot(cx, value); + RootedValue otherRoot(cx, other.value); + MOZ_ASSERT(SameValueZero(cx, valueRoot, otherRoot, &same)); + MOZ_ASSERT(same == b); +#endif + return b; +} + +/*** MapIterator ************************************************************/ + +namespace {} /* anonymous namespace */ + +static const JSClassOps MapIteratorObjectClassOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + MapIteratorObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +static const ClassExtension MapIteratorObjectClassExtension = { + MapIteratorObject::objectMoved, // objectMovedOp +}; + +const JSClass MapIteratorObject::class_ = { + "Map Iterator", + JSCLASS_HAS_RESERVED_SLOTS(MapIteratorObject::SlotCount) | + JSCLASS_FOREGROUND_FINALIZE | JSCLASS_SKIP_NURSERY_FINALIZE, + &MapIteratorObjectClassOps, JS_NULL_CLASS_SPEC, + &MapIteratorObjectClassExtension}; + +const JSFunctionSpec MapIteratorObject::methods[] = { + JS_SELF_HOSTED_FN("next", "MapIteratorNext", 0, 0), JS_FS_END}; + +static inline ValueMap::Range* MapIteratorObjectRange(NativeObject* obj) { + MOZ_ASSERT(obj->is<MapIteratorObject>()); + return obj->maybePtrFromReservedSlot<ValueMap::Range>( + MapIteratorObject::RangeSlot); +} + +inline MapObject::IteratorKind MapIteratorObject::kind() const { + int32_t i = getReservedSlot(KindSlot).toInt32(); + MOZ_ASSERT(i == MapObject::Keys || i == MapObject::Values || + i == MapObject::Entries); + return MapObject::IteratorKind(i); +} + +/* static */ +bool GlobalObject::initMapIteratorProto(JSContext* cx, + Handle<GlobalObject*> global) { + Rooted<JSObject*> base( + cx, GlobalObject::getOrCreateIteratorPrototype(cx, global)); + if (!base) { + return false; + } + Rooted<PlainObject*> proto( + cx, GlobalObject::createBlankPrototypeInheriting<PlainObject>(cx, base)); + if (!proto) { + return false; + } + if (!JS_DefineFunctions(cx, proto, MapIteratorObject::methods) || + !DefineToStringTag(cx, proto, cx->names().MapIterator)) { + return false; + } + global->initBuiltinProto(ProtoKind::MapIteratorProto, proto); + return true; +} + +template <typename TableObject> +static inline bool HasNurseryMemory(TableObject* t) { + return t->getReservedSlot(TableObject::HasNurseryMemorySlot).toBoolean(); +} + +template <typename TableObject> +static inline void SetHasNurseryMemory(TableObject* t, bool b) { + t->setReservedSlot(TableObject::HasNurseryMemorySlot, JS::BooleanValue(b)); +} + +MapIteratorObject* MapIteratorObject::create(JSContext* cx, HandleObject obj, + const ValueMap* data, + MapObject::IteratorKind kind) { + Handle<MapObject*> mapobj(obj.as<MapObject>()); + Rooted<GlobalObject*> global(cx, &mapobj->global()); + Rooted<JSObject*> proto( + cx, GlobalObject::getOrCreateMapIteratorPrototype(cx, global)); + if (!proto) { + return nullptr; + } + + MapIteratorObject* iterobj = + NewObjectWithGivenProto<MapIteratorObject>(cx, proto); + if (!iterobj) { + return nullptr; + } + + iterobj->init(mapobj, kind); + + constexpr size_t BufferSize = + RoundUp(sizeof(ValueMap::Range), gc::CellAlignBytes); + + Nursery& nursery = cx->nursery(); + void* buffer = nursery.allocateBufferSameLocation(iterobj, BufferSize); + if (!buffer) { + // Retry with |iterobj| and |buffer| forcibly tenured. + iterobj = NewTenuredObjectWithGivenProto<MapIteratorObject>(cx, proto); + if (!iterobj) { + return nullptr; + } + + iterobj->init(mapobj, kind); + + buffer = nursery.allocateBufferSameLocation(iterobj, BufferSize); + if (!buffer) { + ReportOutOfMemory(cx); + return nullptr; + } + } + + bool insideNursery = IsInsideNursery(iterobj); + MOZ_ASSERT(insideNursery == nursery.isInside(buffer)); + + if (insideNursery && !HasNurseryMemory(mapobj.get())) { + if (!cx->nursery().addMapWithNurseryMemory(mapobj)) { + ReportOutOfMemory(cx); + return nullptr; + } + SetHasNurseryMemory(mapobj.get(), true); + } + + auto range = data->createRange(buffer, insideNursery); + iterobj->setReservedSlot(RangeSlot, PrivateValue(range)); + + return iterobj; +} + +void MapIteratorObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + MOZ_ASSERT(!IsInsideNursery(obj)); + + auto range = MapIteratorObjectRange(&obj->as<NativeObject>()); + MOZ_ASSERT(!gcx->runtime()->gc.nursery().isInside(range)); + + // Bug 1560019: Malloc memory associated with MapIteratorObjects is not + // currently tracked. + gcx->deleteUntracked(range); +} + +size_t MapIteratorObject::objectMoved(JSObject* obj, JSObject* old) { + if (!IsInsideNursery(old)) { + return 0; + } + + MapIteratorObject* iter = &obj->as<MapIteratorObject>(); + ValueMap::Range* range = MapIteratorObjectRange(iter); + if (!range) { + return 0; + } + + Nursery& nursery = iter->runtimeFromMainThread()->gc.nursery(); + if (!nursery.isInside(range)) { + nursery.removeMallocedBufferDuringMinorGC(range); + return 0; + } + + AutoEnterOOMUnsafeRegion oomUnsafe; + auto newRange = iter->zone()->new_<ValueMap::Range>(*range); + if (!newRange) { + oomUnsafe.crash( + "MapIteratorObject failed to allocate Range data while tenuring."); + } + + range->~Range(); + iter->setReservedSlot(MapIteratorObject::RangeSlot, PrivateValue(newRange)); + return sizeof(ValueMap::Range); +} + +template <typename Range> +static void DestroyRange(JSObject* iterator, Range* range) { + range->~Range(); + if (!IsInsideNursery(iterator)) { + js_free(range); + } +} + +bool MapIteratorObject::next(MapIteratorObject* mapIterator, + ArrayObject* resultPairObj) { + // IC code calls this directly. + AutoUnsafeCallWithABI unsafe; + + // Check invariants for inlined GetNextMapEntryForIterator. + + // The array should be tenured, so that post-barrier can be done simply. + MOZ_ASSERT(resultPairObj->isTenured()); + + // The array elements should be fixed. + MOZ_ASSERT(resultPairObj->hasFixedElements()); + MOZ_ASSERT(resultPairObj->getDenseInitializedLength() == 2); + MOZ_ASSERT(resultPairObj->getDenseCapacity() >= 2); + + ValueMap::Range* range = MapIteratorObjectRange(mapIterator); + if (!range) { + return true; + } + + if (range->empty()) { + DestroyRange<ValueMap::Range>(mapIterator, range); + mapIterator->setReservedSlot(RangeSlot, PrivateValue(nullptr)); + return true; + } + + switch (mapIterator->kind()) { + case MapObject::Keys: + resultPairObj->setDenseElement(0, range->front().key.get()); + break; + + case MapObject::Values: + resultPairObj->setDenseElement(1, range->front().value); + break; + + case MapObject::Entries: { + resultPairObj->setDenseElement(0, range->front().key.get()); + resultPairObj->setDenseElement(1, range->front().value); + break; + } + } + range->popFront(); + return false; +} + +/* static */ +JSObject* MapIteratorObject::createResultPair(JSContext* cx) { + Rooted<ArrayObject*> resultPairObj( + cx, NewDenseFullyAllocatedArray(cx, 2, TenuredObject)); + if (!resultPairObj) { + return nullptr; + } + + resultPairObj->setDenseInitializedLength(2); + resultPairObj->initDenseElement(0, NullValue()); + resultPairObj->initDenseElement(1, NullValue()); + + return resultPairObj; +} + +/*** Map ********************************************************************/ + +struct js::UnbarrieredHashPolicy { + using Lookup = Value; + static HashNumber hash(const Lookup& v, + const mozilla::HashCodeScrambler& hcs) { + return HashValue(v, hcs); + } + static bool match(const Value& k, const Lookup& l) { return k == l; } + static bool isEmpty(const Value& v) { return v.isMagic(JS_HASH_KEY_EMPTY); } + static void makeEmpty(Value* vp) { vp->setMagic(JS_HASH_KEY_EMPTY); } +}; + +// ValueMap, MapObject::UnbarrieredTable and MapObject::PreBarrieredTable must +// all have the same memory layout. +static_assert(sizeof(ValueMap) == sizeof(MapObject::UnbarrieredTable)); +static_assert(sizeof(ValueMap::Entry) == + sizeof(MapObject::UnbarrieredTable::Entry)); +static_assert(sizeof(ValueMap) == sizeof(MapObject::PreBarrieredTable)); +static_assert(sizeof(ValueMap::Entry) == + sizeof(MapObject::PreBarrieredTable::Entry)); + +const JSClassOps MapObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + finalize, // finalize + nullptr, // call + nullptr, // construct + trace, // trace +}; + +const ClassSpec MapObject::classSpec_ = { + GenericCreateConstructor<MapObject::construct, 0, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<MapObject>, + nullptr, + MapObject::staticProperties, + MapObject::methods, + MapObject::properties, + MapObject::finishInit}; + +const JSClass MapObject::class_ = { + "Map", + JSCLASS_DELAY_METADATA_BUILDER | + JSCLASS_HAS_RESERVED_SLOTS(MapObject::SlotCount) | + JSCLASS_HAS_CACHED_PROTO(JSProto_Map) | JSCLASS_FOREGROUND_FINALIZE | + JSCLASS_SKIP_NURSERY_FINALIZE, + &MapObject::classOps_, &MapObject::classSpec_}; + +const JSClass MapObject::protoClass_ = { + "Map.prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_Map), JS_NULL_CLASS_OPS, + &MapObject::classSpec_}; + +const JSPropertySpec MapObject::properties[] = { + JS_PSG("size", size, 0), + JS_STRING_SYM_PS(toStringTag, "Map", JSPROP_READONLY), JS_PS_END}; + +// clang-format off +const JSFunctionSpec MapObject::methods[] = { + JS_INLINABLE_FN("get", get, 1, 0, MapGet), + JS_INLINABLE_FN("has", has, 1, 0, MapHas), + JS_FN("set", set, 2, 0), + JS_FN("delete", delete_, 1, 0), + JS_FN("keys", keys, 0, 0), + JS_FN("values", values, 0, 0), + JS_FN("clear", clear, 0, 0), + JS_SELF_HOSTED_FN("forEach", "MapForEach", 2, 0), + JS_FN("entries", entries, 0, 0), + // @@iterator is re-defined in finishInit so that it has the + // same identity as |entries|. + JS_SYM_FN(iterator, entries, 0, 0), + JS_FS_END +}; +// clang-format on + +const JSPropertySpec MapObject::staticProperties[] = { + JS_SELF_HOSTED_SYM_GET(species, "$MapSpecies", 0), JS_PS_END}; + +/* static */ bool MapObject::finishInit(JSContext* cx, HandleObject ctor, + HandleObject proto) { + Handle<NativeObject*> nativeProto = proto.as<NativeObject>(); + + RootedValue entriesFn(cx); + RootedId entriesId(cx, NameToId(cx->names().entries)); + if (!NativeGetProperty(cx, nativeProto, entriesId, &entriesFn)) { + return false; + } + + // 23.1.3.12 Map.prototype[@@iterator]() + // The initial value of the @@iterator property is the same function object + // as the initial value of the "entries" property. + RootedId iteratorId(cx, PropertyKey::Symbol(cx->wellKnownSymbols().iterator)); + return NativeDefineDataProperty(cx, nativeProto, iteratorId, entriesFn, 0); +} + +void MapObject::trace(JSTracer* trc, JSObject* obj) { + if (ValueMap* map = obj->as<MapObject>().getTableUnchecked()) { + map->trace(trc); + } +} + +using NurseryKeysVector = mozilla::Vector<Value, 0, SystemAllocPolicy>; + +template <typename TableObject> +static NurseryKeysVector* GetNurseryKeys(TableObject* t) { + Value value = t->getReservedSlot(TableObject::NurseryKeysSlot); + return reinterpret_cast<NurseryKeysVector*>(value.toPrivate()); +} + +template <typename TableObject> +static NurseryKeysVector* AllocNurseryKeys(TableObject* t) { + MOZ_ASSERT(!GetNurseryKeys(t)); + auto keys = js_new<NurseryKeysVector>(); + if (!keys) { + return nullptr; + } + + t->setReservedSlot(TableObject::NurseryKeysSlot, PrivateValue(keys)); + return keys; +} + +template <typename TableObject> +static void DeleteNurseryKeys(TableObject* t) { + auto keys = GetNurseryKeys(t); + MOZ_ASSERT(keys); + js_delete(keys); + t->setReservedSlot(TableObject::NurseryKeysSlot, PrivateValue(nullptr)); +} + +// A generic store buffer entry that traces all nursery keys for an ordered hash +// map or set. +template <typename ObjectT> +class js::OrderedHashTableRef : public gc::BufferableRef { + ObjectT* object; + + public: + explicit OrderedHashTableRef(ObjectT* obj) : object(obj) {} + + void trace(JSTracer* trc) override { + MOZ_ASSERT(!IsInsideNursery(object)); + auto realTable = object->getTableUnchecked(); + auto unbarrieredTable = + reinterpret_cast<typename ObjectT::UnbarrieredTable*>(realTable); + NurseryKeysVector* keys = GetNurseryKeys(object); + MOZ_ASSERT(keys); + for (Value key : *keys) { + MOZ_ASSERT(unbarrieredTable->hash(key) == + realTable->hash(*reinterpret_cast<HashableValue*>(&key))); + // Note: we use a lambda to avoid tenuring keys that have been removed + // from the Map or Set. + unbarrieredTable->rekeyOneEntry(key, [trc](const Value& prior) { + Value key = prior; + TraceManuallyBarrieredEdge(trc, &key, "ordered hash table key"); + return key; + }); + } + DeleteNurseryKeys(object); + } +}; + +template <typename ObjectT> +[[nodiscard]] inline static bool PostWriteBarrierImpl(ObjectT* obj, + const Value& keyValue) { + if (MOZ_LIKELY(!keyValue.hasObjectPayload() && !keyValue.isBigInt())) { + MOZ_ASSERT_IF(keyValue.isGCThing(), !IsInsideNursery(keyValue.toGCThing())); + return true; + } + + if (!IsInsideNursery(keyValue.toGCThing())) { + return true; + } + + NurseryKeysVector* keys = GetNurseryKeys(obj); + if (!keys) { + keys = AllocNurseryKeys(obj); + if (!keys) { + return false; + } + + keyValue.toGCThing()->storeBuffer()->putGeneric( + OrderedHashTableRef<ObjectT>(obj)); + } + + return keys->append(keyValue); +} + +[[nodiscard]] inline static bool PostWriteBarrier(MapObject* map, + const Value& key) { + MOZ_ASSERT(!IsInsideNursery(map)); + return PostWriteBarrierImpl(map, key); +} + +[[nodiscard]] inline static bool PostWriteBarrier(SetObject* set, + const Value& key) { + if (IsInsideNursery(set)) { + return true; + } + + return PostWriteBarrierImpl(set, key); +} + +bool MapObject::getKeysAndValuesInterleaved( + HandleObject obj, JS::MutableHandle<GCVector<JS::Value>> entries) { + const ValueMap* map = obj->as<MapObject>().getData(); + if (!map) { + return false; + } + + for (ValueMap::Range r = map->all(); !r.empty(); r.popFront()) { + if (!entries.append(r.front().key.get()) || + !entries.append(r.front().value)) { + return false; + } + } + + return true; +} + +bool MapObject::set(JSContext* cx, HandleObject obj, HandleValue k, + HandleValue v) { + MapObject* mapObject = &obj->as<MapObject>(); + Rooted<HashableValue> key(cx); + if (!key.setValue(cx, k)) { + return false; + } + + return setWithHashableKey(cx, mapObject, key, v); +} + +/* static */ +inline bool MapObject::setWithHashableKey(JSContext* cx, MapObject* obj, + Handle<HashableValue> key, + Handle<Value> value) { + ValueMap* table = obj->getTableUnchecked(); + if (!table) { + return false; + } + + bool needsPostBarriers = obj->isTenured(); + if (needsPostBarriers) { + // Use the ValueMap representation which has post barriers. + if (!PostWriteBarrier(obj, key.get()) || !table->put(key.get(), value)) { + ReportOutOfMemory(cx); + return false; + } + } else { + // Use the PreBarrieredTable representation which does not. + auto* preBarriedTable = reinterpret_cast<PreBarrieredTable*>(table); + if (!preBarriedTable->put(key.get(), value.get())) { + ReportOutOfMemory(cx); + return false; + } + } + + return true; +} + +MapObject* MapObject::create(JSContext* cx, + HandleObject proto /* = nullptr */) { + auto map = cx->make_unique<ValueMap>(cx->zone(), + cx->realm()->randomHashCodeScrambler()); + if (!map) { + return nullptr; + } + + if (!map->init()) { + ReportOutOfMemory(cx); + return nullptr; + } + + AutoSetNewObjectMetadata metadata(cx); + MapObject* mapObj = NewObjectWithClassProto<MapObject>(cx, proto); + if (!mapObj) { + return nullptr; + } + + bool insideNursery = IsInsideNursery(mapObj); + if (insideNursery && !cx->nursery().addMapWithNurseryMemory(mapObj)) { + ReportOutOfMemory(cx); + return nullptr; + } + + InitReservedSlot(mapObj, DataSlot, map.release(), MemoryUse::MapObjectTable); + mapObj->initReservedSlot(NurseryKeysSlot, PrivateValue(nullptr)); + mapObj->initReservedSlot(HasNurseryMemorySlot, + JS::BooleanValue(insideNursery)); + return mapObj; +} + +size_t MapObject::sizeOfData(mozilla::MallocSizeOf mallocSizeOf) { + size_t size = 0; + if (const ValueMap* map = getData()) { + size += map->sizeOfIncludingThis(mallocSizeOf); + } + if (NurseryKeysVector* nurseryKeys = GetNurseryKeys(this)) { + size += nurseryKeys->sizeOfIncludingThis(mallocSizeOf); + } + return size; +} + +void MapObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + ValueMap* table = obj->as<MapObject>().getTableUnchecked(); + if (!table) { + return; + } + + bool needsPostBarriers = obj->isTenured(); + if (needsPostBarriers) { + // Use the ValueMap representation which has post barriers. + gcx->delete_(obj, table, MemoryUse::MapObjectTable); + } else { + // Use the PreBarrieredTable representation which does not. + auto* preBarriedTable = reinterpret_cast<PreBarrieredTable*>(table); + gcx->delete_(obj, preBarriedTable, MemoryUse::MapObjectTable); + } +} + +/* static */ +void MapObject::sweepAfterMinorGC(JS::GCContext* gcx, MapObject* mapobj) { + bool wasInsideNursery = IsInsideNursery(mapobj); + if (wasInsideNursery && !IsForwarded(mapobj)) { + finalize(gcx, mapobj); + return; + } + + mapobj = MaybeForwarded(mapobj); + mapobj->getTableUnchecked()->destroyNurseryRanges(); + SetHasNurseryMemory(mapobj, false); + + if (wasInsideNursery) { + AddCellMemory(mapobj, sizeof(ValueMap), MemoryUse::MapObjectTable); + } +} + +bool MapObject::construct(JSContext* cx, unsigned argc, Value* vp) { + AutoJSConstructorProfilerEntry pseudoFrame(cx, "Map"); + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "Map")) { + return false; + } + + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Map, &proto)) { + return false; + } + + Rooted<MapObject*> obj(cx, MapObject::create(cx, proto)); + if (!obj) { + return false; + } + + if (!args.get(0).isNullOrUndefined()) { + FixedInvokeArgs<1> args2(cx); + args2[0].set(args[0]); + + RootedValue thisv(cx, ObjectValue(*obj)); + if (!CallSelfHostedFunction(cx, cx->names().MapConstructorInit, thisv, + args2, args2.rval())) { + return false; + } + } + + args.rval().setObject(*obj); + return true; +} + +bool MapObject::is(HandleValue v) { + return v.isObject() && v.toObject().hasClass(&class_) && + !v.toObject().as<MapObject>().getReservedSlot(DataSlot).isUndefined(); +} + +bool MapObject::is(HandleObject o) { + return o->hasClass(&class_) && + !o->as<MapObject>().getReservedSlot(DataSlot).isUndefined(); +} + +#define ARG0_KEY(cx, args, key) \ + Rooted<HashableValue> key(cx); \ + if (args.length() > 0 && !key.setValue(cx, args[0])) return false + +const ValueMap& MapObject::extract(HandleObject o) { + MOZ_ASSERT(o->hasClass(&MapObject::class_)); + return *o->as<MapObject>().getData(); +} + +const ValueMap& MapObject::extract(const CallArgs& args) { + MOZ_ASSERT(args.thisv().isObject()); + MOZ_ASSERT(args.thisv().toObject().hasClass(&MapObject::class_)); + return *args.thisv().toObject().as<MapObject>().getData(); +} + +uint32_t MapObject::size(JSContext* cx, HandleObject obj) { + const ValueMap& map = extract(obj); + static_assert(sizeof(map.count()) <= sizeof(uint32_t), + "map count must be precisely representable as a JS number"); + return map.count(); +} + +bool MapObject::size_impl(JSContext* cx, const CallArgs& args) { + RootedObject obj(cx, &args.thisv().toObject()); + args.rval().setNumber(size(cx, obj)); + return true; +} + +bool MapObject::size(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "size"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<MapObject::is, MapObject::size_impl>(cx, args); +} + +bool MapObject::get(JSContext* cx, HandleObject obj, HandleValue key, + MutableHandleValue rval) { + const ValueMap& map = extract(obj); + Rooted<HashableValue> k(cx); + + if (!k.setValue(cx, key)) { + return false; + } + + if (const ValueMap::Entry* p = map.get(k)) { + rval.set(p->value); + } else { + rval.setUndefined(); + } + + return true; +} + +bool MapObject::get_impl(JSContext* cx, const CallArgs& args) { + RootedObject obj(cx, &args.thisv().toObject()); + return get(cx, obj, args.get(0), args.rval()); +} + +bool MapObject::get(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "get"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<MapObject::is, MapObject::get_impl>(cx, args); +} + +bool MapObject::has(JSContext* cx, HandleObject obj, HandleValue key, + bool* rval) { + const ValueMap& map = extract(obj); + Rooted<HashableValue> k(cx); + + if (!k.setValue(cx, key)) { + return false; + } + + *rval = map.has(k); + return true; +} + +bool MapObject::has_impl(JSContext* cx, const CallArgs& args) { + bool found; + RootedObject obj(cx, &args.thisv().toObject()); + if (has(cx, obj, args.get(0), &found)) { + args.rval().setBoolean(found); + return true; + } + return false; +} + +bool MapObject::has(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "has"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<MapObject::is, MapObject::has_impl>(cx, args); +} + +bool MapObject::set_impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(MapObject::is(args.thisv())); + + MapObject* obj = &args.thisv().toObject().as<MapObject>(); + ARG0_KEY(cx, args, key); + if (!setWithHashableKey(cx, obj, key, args.get(1))) { + return false; + } + + args.rval().set(args.thisv()); + return true; +} + +bool MapObject::set(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "set"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<MapObject::is, MapObject::set_impl>(cx, args); +} + +bool MapObject::delete_(JSContext* cx, HandleObject obj, HandleValue key, + bool* rval) { + MapObject* mapObject = &obj->as<MapObject>(); + Rooted<HashableValue> k(cx); + + if (!k.setValue(cx, key)) { + return false; + } + + bool ok; + if (mapObject->isTenured()) { + ok = mapObject->tenuredTable()->remove(k, rval); + } else { + ok = mapObject->nurseryTable()->remove(k, rval); + } + + if (!ok) { + ReportOutOfMemory(cx); + return false; + } + + return true; +} + +bool MapObject::delete_impl(JSContext* cx, const CallArgs& args) { + // MapObject::trace does not trace deleted entries. Incremental GC therefore + // requires that no HeapPtr<Value> objects pointing to heap values be left + // alive in the ValueMap. + // + // OrderedHashMap::remove() doesn't destroy the removed entry. It merely + // calls OrderedHashMap::MapOps::makeEmpty. But that is sufficient, because + // makeEmpty clears the value by doing e->value = Value(), and in the case + // of a ValueMap, Value() means HeapPtr<Value>(), which is the same as + // HeapPtr<Value>(UndefinedValue()). + MOZ_ASSERT(MapObject::is(args.thisv())); + RootedObject obj(cx, &args.thisv().toObject()); + + bool found; + if (!delete_(cx, obj, args.get(0), &found)) { + return false; + } + + args.rval().setBoolean(found); + return true; +} + +bool MapObject::delete_(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "delete"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<MapObject::is, MapObject::delete_impl>(cx, args); +} + +bool MapObject::iterator(JSContext* cx, IteratorKind kind, HandleObject obj, + MutableHandleValue iter) { + const ValueMap& map = extract(obj); + Rooted<JSObject*> iterobj(cx, MapIteratorObject::create(cx, obj, &map, kind)); + if (!iterobj) { + return false; + } + iter.setObject(*iterobj); + return true; +} + +bool MapObject::iterator_impl(JSContext* cx, const CallArgs& args, + IteratorKind kind) { + RootedObject obj(cx, &args.thisv().toObject()); + return iterator(cx, kind, obj, args.rval()); +} + +bool MapObject::keys_impl(JSContext* cx, const CallArgs& args) { + return iterator_impl(cx, args, Keys); +} + +bool MapObject::keys(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "keys"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, is, keys_impl, args); +} + +bool MapObject::values_impl(JSContext* cx, const CallArgs& args) { + return iterator_impl(cx, args, Values); +} + +bool MapObject::values(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "values"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, is, values_impl, args); +} + +bool MapObject::entries_impl(JSContext* cx, const CallArgs& args) { + return iterator_impl(cx, args, Entries); +} + +bool MapObject::entries(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "entries"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, is, entries_impl, args); +} + +bool MapObject::clear_impl(JSContext* cx, const CallArgs& args) { + RootedObject obj(cx, &args.thisv().toObject()); + args.rval().setUndefined(); + return clear(cx, obj); +} + +bool MapObject::clear(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Map.prototype", "clear"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, is, clear_impl, args); +} + +bool MapObject::clear(JSContext* cx, HandleObject obj) { + MapObject* mapObject = &obj->as<MapObject>(); + + bool ok; + if (mapObject->isTenured()) { + ok = mapObject->tenuredTable()->clear(); + } else { + ok = mapObject->nurseryTable()->clear(); + } + + if (!ok) { + ReportOutOfMemory(cx); + return false; + } + + return true; +} + +/*** SetIterator ************************************************************/ + +static const JSClassOps SetIteratorObjectClassOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + SetIteratorObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +static const ClassExtension SetIteratorObjectClassExtension = { + SetIteratorObject::objectMoved, // objectMovedOp +}; + +const JSClass SetIteratorObject::class_ = { + "Set Iterator", + JSCLASS_HAS_RESERVED_SLOTS(SetIteratorObject::SlotCount) | + JSCLASS_FOREGROUND_FINALIZE | JSCLASS_SKIP_NURSERY_FINALIZE, + &SetIteratorObjectClassOps, JS_NULL_CLASS_SPEC, + &SetIteratorObjectClassExtension}; + +const JSFunctionSpec SetIteratorObject::methods[] = { + JS_SELF_HOSTED_FN("next", "SetIteratorNext", 0, 0), JS_FS_END}; + +static inline ValueSet::Range* SetIteratorObjectRange(NativeObject* obj) { + MOZ_ASSERT(obj->is<SetIteratorObject>()); + return obj->maybePtrFromReservedSlot<ValueSet::Range>( + SetIteratorObject::RangeSlot); +} + +inline SetObject::IteratorKind SetIteratorObject::kind() const { + int32_t i = getReservedSlot(KindSlot).toInt32(); + MOZ_ASSERT(i == SetObject::Values || i == SetObject::Entries); + return SetObject::IteratorKind(i); +} + +/* static */ +bool GlobalObject::initSetIteratorProto(JSContext* cx, + Handle<GlobalObject*> global) { + Rooted<JSObject*> base( + cx, GlobalObject::getOrCreateIteratorPrototype(cx, global)); + if (!base) { + return false; + } + Rooted<PlainObject*> proto( + cx, GlobalObject::createBlankPrototypeInheriting<PlainObject>(cx, base)); + if (!proto) { + return false; + } + if (!JS_DefineFunctions(cx, proto, SetIteratorObject::methods) || + !DefineToStringTag(cx, proto, cx->names().SetIterator)) { + return false; + } + global->initBuiltinProto(ProtoKind::SetIteratorProto, proto); + return true; +} + +SetIteratorObject* SetIteratorObject::create(JSContext* cx, HandleObject obj, + ValueSet* data, + SetObject::IteratorKind kind) { + MOZ_ASSERT(kind != SetObject::Keys); + + Handle<SetObject*> setobj(obj.as<SetObject>()); + Rooted<GlobalObject*> global(cx, &setobj->global()); + Rooted<JSObject*> proto( + cx, GlobalObject::getOrCreateSetIteratorPrototype(cx, global)); + if (!proto) { + return nullptr; + } + + SetIteratorObject* iterobj = + NewObjectWithGivenProto<SetIteratorObject>(cx, proto); + if (!iterobj) { + return nullptr; + } + + iterobj->init(setobj, kind); + + constexpr size_t BufferSize = + RoundUp(sizeof(ValueSet::Range), gc::CellAlignBytes); + + Nursery& nursery = cx->nursery(); + void* buffer = nursery.allocateBufferSameLocation(iterobj, BufferSize); + if (!buffer) { + // Retry with |iterobj| and |buffer| forcibly tenured. + iterobj = NewTenuredObjectWithGivenProto<SetIteratorObject>(cx, proto); + if (!iterobj) { + return nullptr; + } + + iterobj->init(setobj, kind); + + buffer = nursery.allocateBufferSameLocation(iterobj, BufferSize); + if (!buffer) { + ReportOutOfMemory(cx); + return nullptr; + } + } + + bool insideNursery = IsInsideNursery(iterobj); + MOZ_ASSERT(insideNursery == nursery.isInside(buffer)); + + if (insideNursery && !HasNurseryMemory(setobj.get())) { + if (!cx->nursery().addSetWithNurseryMemory(setobj)) { + ReportOutOfMemory(cx); + return nullptr; + } + SetHasNurseryMemory(setobj.get(), true); + } + + auto range = data->createRange(buffer, insideNursery); + iterobj->setReservedSlot(RangeSlot, PrivateValue(range)); + + return iterobj; +} + +void SetIteratorObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + MOZ_ASSERT(!IsInsideNursery(obj)); + + auto range = SetIteratorObjectRange(&obj->as<NativeObject>()); + MOZ_ASSERT(!gcx->runtime()->gc.nursery().isInside(range)); + + // Bug 1560019: Malloc memory associated with SetIteratorObjects is not + // currently tracked. + gcx->deleteUntracked(range); +} + +size_t SetIteratorObject::objectMoved(JSObject* obj, JSObject* old) { + if (!IsInsideNursery(old)) { + return 0; + } + + SetIteratorObject* iter = &obj->as<SetIteratorObject>(); + ValueSet::Range* range = SetIteratorObjectRange(iter); + if (!range) { + return 0; + } + + Nursery& nursery = iter->runtimeFromMainThread()->gc.nursery(); + if (!nursery.isInside(range)) { + nursery.removeMallocedBufferDuringMinorGC(range); + return 0; + } + + AutoEnterOOMUnsafeRegion oomUnsafe; + auto newRange = iter->zone()->new_<ValueSet::Range>(*range); + if (!newRange) { + oomUnsafe.crash( + "SetIteratorObject failed to allocate Range data while tenuring."); + } + + range->~Range(); + iter->setReservedSlot(SetIteratorObject::RangeSlot, PrivateValue(newRange)); + return sizeof(ValueSet::Range); +} + +bool SetIteratorObject::next(SetIteratorObject* setIterator, + ArrayObject* resultObj) { + // IC code calls this directly. + AutoUnsafeCallWithABI unsafe; + + // Check invariants for inlined _GetNextSetEntryForIterator. + + // The array should be tenured, so that post-barrier can be done simply. + MOZ_ASSERT(resultObj->isTenured()); + + // The array elements should be fixed. + MOZ_ASSERT(resultObj->hasFixedElements()); + MOZ_ASSERT(resultObj->getDenseInitializedLength() == 1); + MOZ_ASSERT(resultObj->getDenseCapacity() >= 1); + + ValueSet::Range* range = SetIteratorObjectRange(setIterator); + if (!range) { + return true; + } + + if (range->empty()) { + DestroyRange<ValueSet::Range>(setIterator, range); + setIterator->setReservedSlot(RangeSlot, PrivateValue(nullptr)); + return true; + } + + resultObj->setDenseElement(0, range->front().get()); + range->popFront(); + return false; +} + +/* static */ +JSObject* SetIteratorObject::createResult(JSContext* cx) { + Rooted<ArrayObject*> resultObj( + cx, NewDenseFullyAllocatedArray(cx, 1, TenuredObject)); + if (!resultObj) { + return nullptr; + } + + resultObj->setDenseInitializedLength(1); + resultObj->initDenseElement(0, NullValue()); + + return resultObj; +} + +/*** Set ********************************************************************/ + +const JSClassOps SetObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + finalize, // finalize + nullptr, // call + nullptr, // construct + trace, // trace +}; + +const ClassSpec SetObject::classSpec_ = { + GenericCreateConstructor<SetObject::construct, 0, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<SetObject>, + nullptr, + SetObject::staticProperties, + SetObject::methods, + SetObject::properties, + SetObject::finishInit}; + +const JSClass SetObject::class_ = { + "Set", + JSCLASS_DELAY_METADATA_BUILDER | + JSCLASS_HAS_RESERVED_SLOTS(SetObject::SlotCount) | + JSCLASS_HAS_CACHED_PROTO(JSProto_Set) | JSCLASS_FOREGROUND_FINALIZE | + JSCLASS_SKIP_NURSERY_FINALIZE, + &SetObject::classOps_, + &SetObject::classSpec_, +}; + +const JSClass SetObject::protoClass_ = { + "Set.prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_Set), JS_NULL_CLASS_OPS, + &SetObject::classSpec_}; + +const JSPropertySpec SetObject::properties[] = { + JS_PSG("size", size, 0), + JS_STRING_SYM_PS(toStringTag, "Set", JSPROP_READONLY), JS_PS_END}; + +// clang-format off +const JSFunctionSpec SetObject::methods[] = { + JS_INLINABLE_FN("has", has, 1, 0, SetHas), + JS_FN("add", add, 1, 0), + JS_FN("delete", delete_, 1, 0), + JS_FN("entries", entries, 0, 0), + JS_FN("clear", clear, 0, 0), + JS_SELF_HOSTED_FN("forEach", "SetForEach", 2, 0), +#ifdef ENABLE_NEW_SET_METHODS + JS_SELF_HOSTED_FN("union", "SetUnion", 1, 0), + JS_SELF_HOSTED_FN("difference", "SetDifference", 1, 0), + JS_SELF_HOSTED_FN("intersection", "SetIntersection", 1, 0), + JS_SELF_HOSTED_FN("symmetricDifference", "SetSymmetricDifference", 1, 0), + JS_SELF_HOSTED_FN("isSubsetOf", "SetIsSubsetOf", 1, 0), + JS_SELF_HOSTED_FN("isSupersetOf", "SetIsSupersetOf", 1, 0), + JS_SELF_HOSTED_FN("isDisjointFrom", "SetIsDisjointFrom", 1, 0), +#endif + JS_FN("values", values, 0, 0), + // @@iterator and |keys| re-defined in finishInit so that they have the + // same identity as |values|. + JS_FN("keys", values, 0, 0), + JS_SYM_FN(iterator, values, 0, 0), + JS_FS_END +}; +// clang-format on + +const JSPropertySpec SetObject::staticProperties[] = { + JS_SELF_HOSTED_SYM_GET(species, "$SetSpecies", 0), JS_PS_END}; + +/* static */ bool SetObject::finishInit(JSContext* cx, HandleObject ctor, + HandleObject proto) { + Handle<NativeObject*> nativeProto = proto.as<NativeObject>(); + + RootedValue valuesFn(cx); + RootedId valuesId(cx, NameToId(cx->names().values)); + if (!NativeGetProperty(cx, nativeProto, valuesId, &valuesFn)) { + return false; + } + + // 23.2.3.8 Set.prototype.keys() + // The initial value of the "keys" property is the same function object + // as the initial value of the "values" property. + RootedId keysId(cx, NameToId(cx->names().keys)); + if (!NativeDefineDataProperty(cx, nativeProto, keysId, valuesFn, 0)) { + return false; + } + + // 23.2.3.11 Set.prototype[@@iterator]() + // See above. + RootedId iteratorId(cx, PropertyKey::Symbol(cx->wellKnownSymbols().iterator)); + return NativeDefineDataProperty(cx, nativeProto, iteratorId, valuesFn, 0); +} + +bool SetObject::keys(JSContext* cx, HandleObject obj, + JS::MutableHandle<GCVector<JS::Value>> keys) { + ValueSet* set = obj->as<SetObject>().getData(); + if (!set) { + return false; + } + + for (ValueSet::Range r = set->all(); !r.empty(); r.popFront()) { + if (!keys.append(r.front().get())) { + return false; + } + } + + return true; +} + +bool SetObject::add(JSContext* cx, HandleObject obj, HandleValue k) { + ValueSet* set = obj->as<SetObject>().getData(); + if (!set) { + return false; + } + + Rooted<HashableValue> key(cx); + if (!key.setValue(cx, k)) { + return false; + } + + if (!PostWriteBarrier(&obj->as<SetObject>(), key.get()) || + !set->put(key.get())) { + ReportOutOfMemory(cx); + return false; + } + return true; +} + +SetObject* SetObject::create(JSContext* cx, + HandleObject proto /* = nullptr */) { + auto set = cx->make_unique<ValueSet>(cx->zone(), + cx->realm()->randomHashCodeScrambler()); + if (!set) { + return nullptr; + } + + if (!set->init()) { + ReportOutOfMemory(cx); + return nullptr; + } + + AutoSetNewObjectMetadata metadata(cx); + SetObject* obj = NewObjectWithClassProto<SetObject>(cx, proto); + if (!obj) { + return nullptr; + } + + bool insideNursery = IsInsideNursery(obj); + if (insideNursery && !cx->nursery().addSetWithNurseryMemory(obj)) { + ReportOutOfMemory(cx); + return nullptr; + } + + InitReservedSlot(obj, DataSlot, set.release(), MemoryUse::MapObjectTable); + obj->initReservedSlot(NurseryKeysSlot, PrivateValue(nullptr)); + obj->initReservedSlot(HasNurseryMemorySlot, JS::BooleanValue(insideNursery)); + return obj; +} + +void SetObject::trace(JSTracer* trc, JSObject* obj) { + SetObject* setobj = static_cast<SetObject*>(obj); + if (ValueSet* set = setobj->getData()) { + set->trace(trc); + } +} + +size_t SetObject::sizeOfData(mozilla::MallocSizeOf mallocSizeOf) { + size_t size = 0; + if (ValueSet* set = getData()) { + size += set->sizeOfIncludingThis(mallocSizeOf); + } + if (NurseryKeysVector* nurseryKeys = GetNurseryKeys(this)) { + size += nurseryKeys->sizeOfIncludingThis(mallocSizeOf); + } + return size; +} + +void SetObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + SetObject* setobj = static_cast<SetObject*>(obj); + if (ValueSet* set = setobj->getData()) { + gcx->delete_(obj, set, MemoryUse::MapObjectTable); + } +} + +/* static */ +void SetObject::sweepAfterMinorGC(JS::GCContext* gcx, SetObject* setobj) { + bool wasInsideNursery = IsInsideNursery(setobj); + if (wasInsideNursery && !IsForwarded(setobj)) { + finalize(gcx, setobj); + return; + } + + setobj = MaybeForwarded(setobj); + setobj->getData()->destroyNurseryRanges(); + SetHasNurseryMemory(setobj, false); + + if (wasInsideNursery) { + AddCellMemory(setobj, sizeof(ValueSet), MemoryUse::MapObjectTable); + } +} + +bool SetObject::isBuiltinAdd(HandleValue add) { + return IsNativeFunction(add, SetObject::add); +} + +bool SetObject::construct(JSContext* cx, unsigned argc, Value* vp) { + AutoJSConstructorProfilerEntry pseudoFrame(cx, "Set"); + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "Set")) { + return false; + } + + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Set, &proto)) { + return false; + } + + Rooted<SetObject*> obj(cx, SetObject::create(cx, proto)); + if (!obj) { + return false; + } + + if (!args.get(0).isNullOrUndefined()) { + RootedValue iterable(cx, args[0]); + bool optimized = false; + if (!IsOptimizableInitForSet<GlobalObject::getOrCreateSetPrototype, + isBuiltinAdd>(cx, obj, iterable, &optimized)) { + return false; + } + + if (optimized) { + RootedValue keyVal(cx); + Rooted<HashableValue> key(cx); + ValueSet* set = obj->getData(); + Rooted<ArrayObject*> array(cx, &iterable.toObject().as<ArrayObject>()); + for (uint32_t index = 0; index < array->getDenseInitializedLength(); + ++index) { + keyVal.set(array->getDenseElement(index)); + MOZ_ASSERT(!keyVal.isMagic(JS_ELEMENTS_HOLE)); + + if (!key.setValue(cx, keyVal)) { + return false; + } + if (!PostWriteBarrier(obj, key.get()) || !set->put(key.get())) { + ReportOutOfMemory(cx); + return false; + } + } + } else { + FixedInvokeArgs<1> args2(cx); + args2[0].set(args[0]); + + RootedValue thisv(cx, ObjectValue(*obj)); + if (!CallSelfHostedFunction(cx, cx->names().SetConstructorInit, thisv, + args2, args2.rval())) { + return false; + } + } + } + + args.rval().setObject(*obj); + return true; +} + +bool SetObject::is(HandleValue v) { + return v.isObject() && v.toObject().hasClass(&class_) && + !v.toObject().as<SetObject>().getReservedSlot(DataSlot).isUndefined(); +} + +bool SetObject::is(HandleObject o) { + return o->hasClass(&class_) && + !o->as<SetObject>().getReservedSlot(DataSlot).isUndefined(); +} + +ValueSet& SetObject::extract(HandleObject o) { + MOZ_ASSERT(o->hasClass(&SetObject::class_)); + return *o->as<SetObject>().getData(); +} + +ValueSet& SetObject::extract(const CallArgs& args) { + MOZ_ASSERT(args.thisv().isObject()); + MOZ_ASSERT(args.thisv().toObject().hasClass(&SetObject::class_)); + return *static_cast<SetObject&>(args.thisv().toObject()).getData(); +} + +uint32_t SetObject::size(JSContext* cx, HandleObject obj) { + MOZ_ASSERT(SetObject::is(obj)); + ValueSet& set = extract(obj); + static_assert(sizeof(set.count()) <= sizeof(uint32_t), + "set count must be precisely representable as a JS number"); + return set.count(); +} + +bool SetObject::size_impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + ValueSet& set = extract(args); + static_assert(sizeof(set.count()) <= sizeof(uint32_t), + "set count must be precisely representable as a JS number"); + args.rval().setNumber(set.count()); + return true; +} + +bool SetObject::size(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "size"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<SetObject::is, SetObject::size_impl>(cx, args); +} + +bool SetObject::has_impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + ValueSet& set = extract(args); + ARG0_KEY(cx, args, key); + args.rval().setBoolean(set.has(key)); + return true; +} + +bool SetObject::has(JSContext* cx, HandleObject obj, HandleValue key, + bool* rval) { + MOZ_ASSERT(SetObject::is(obj)); + + ValueSet& set = extract(obj); + Rooted<HashableValue> k(cx); + + if (!k.setValue(cx, key)) { + return false; + } + + *rval = set.has(k); + return true; +} + +bool SetObject::has(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "has"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<SetObject::is, SetObject::has_impl>(cx, args); +} + +bool SetObject::add_impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + ValueSet& set = extract(args); + ARG0_KEY(cx, args, key); + if (!PostWriteBarrier(&args.thisv().toObject().as<SetObject>(), key.get()) || + !set.put(key.get())) { + ReportOutOfMemory(cx); + return false; + } + args.rval().set(args.thisv()); + return true; +} + +bool SetObject::add(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "add"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<SetObject::is, SetObject::add_impl>(cx, args); +} + +bool SetObject::delete_(JSContext* cx, HandleObject obj, HandleValue key, + bool* rval) { + MOZ_ASSERT(SetObject::is(obj)); + + ValueSet& set = extract(obj); + Rooted<HashableValue> k(cx); + + if (!k.setValue(cx, key)) { + return false; + } + + if (!set.remove(k, rval)) { + ReportOutOfMemory(cx); + return false; + } + return true; +} + +bool SetObject::delete_impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + ValueSet& set = extract(args); + ARG0_KEY(cx, args, key); + bool found; + if (!set.remove(key, &found)) { + ReportOutOfMemory(cx); + return false; + } + args.rval().setBoolean(found); + return true; +} + +bool SetObject::delete_(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "delete"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<SetObject::is, SetObject::delete_impl>(cx, args); +} + +bool SetObject::iterator(JSContext* cx, IteratorKind kind, HandleObject obj, + MutableHandleValue iter) { + MOZ_ASSERT(SetObject::is(obj)); + ValueSet& set = extract(obj); + Rooted<JSObject*> iterobj(cx, SetIteratorObject::create(cx, obj, &set, kind)); + if (!iterobj) { + return false; + } + iter.setObject(*iterobj); + return true; +} + +bool SetObject::iterator_impl(JSContext* cx, const CallArgs& args, + IteratorKind kind) { + Rooted<SetObject*> setobj(cx, &args.thisv().toObject().as<SetObject>()); + ValueSet& set = *setobj->getData(); + Rooted<JSObject*> iterobj(cx, + SetIteratorObject::create(cx, setobj, &set, kind)); + if (!iterobj) { + return false; + } + args.rval().setObject(*iterobj); + return true; +} + +bool SetObject::values_impl(JSContext* cx, const CallArgs& args) { + return iterator_impl(cx, args, Values); +} + +bool SetObject::values(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "values"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, is, values_impl, args); +} + +bool SetObject::entries_impl(JSContext* cx, const CallArgs& args) { + return iterator_impl(cx, args, Entries); +} + +bool SetObject::entries(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "entries"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, is, entries_impl, args); +} + +bool SetObject::clear(JSContext* cx, HandleObject obj) { + MOZ_ASSERT(SetObject::is(obj)); + ValueSet& set = extract(obj); + if (!set.clear()) { + ReportOutOfMemory(cx); + return false; + } + return true; +} + +bool SetObject::clear_impl(JSContext* cx, const CallArgs& args) { + Rooted<SetObject*> setobj(cx, &args.thisv().toObject().as<SetObject>()); + if (!setobj->getData()->clear()) { + ReportOutOfMemory(cx); + return false; + } + args.rval().setUndefined(); + return true; +} + +bool SetObject::clear(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Set.prototype", "clear"); + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, is, clear_impl, args); +} + +/*** JS static utility functions ********************************************/ + +static bool forEach(const char* funcName, JSContext* cx, HandleObject obj, + HandleValue callbackFn, HandleValue thisArg) { + CHECK_THREAD(cx); + + RootedId forEachId(cx, NameToId(cx->names().forEach)); + RootedFunction forEachFunc( + cx, JS::GetSelfHostedFunction(cx, funcName, forEachId, 2)); + if (!forEachFunc) { + return false; + } + + RootedValue fval(cx, ObjectValue(*forEachFunc)); + return Call(cx, fval, obj, callbackFn, thisArg, &fval); +} + +// Handles Clear/Size for public jsapi map/set access +template <typename RetT> +RetT CallObjFunc(RetT (*ObjFunc)(JSContext*, HandleObject), JSContext* cx, + HandleObject obj) { + CHECK_THREAD(cx); + cx->check(obj); + + // Always unwrap, in case this is an xray or cross-compartment wrapper. + RootedObject unwrappedObj(cx); + unwrappedObj = UncheckedUnwrap(obj); + + // Enter the realm of the backing object before calling functions on + // it. + JSAutoRealm ar(cx, unwrappedObj); + return ObjFunc(cx, unwrappedObj); +} + +// Handles Has/Delete for public jsapi map/set access +bool CallObjFunc(bool (*ObjFunc)(JSContext* cx, HandleObject obj, + HandleValue key, bool* rval), + JSContext* cx, HandleObject obj, HandleValue key, bool* rval) { + CHECK_THREAD(cx); + cx->check(obj, key); + + // Always unwrap, in case this is an xray or cross-compartment wrapper. + RootedObject unwrappedObj(cx); + unwrappedObj = UncheckedUnwrap(obj); + JSAutoRealm ar(cx, unwrappedObj); + + // If we're working with a wrapped map/set, rewrap the key into the + // compartment of the unwrapped map/set. + RootedValue wrappedKey(cx, key); + if (obj != unwrappedObj) { + if (!JS_WrapValue(cx, &wrappedKey)) { + return false; + } + } + return ObjFunc(cx, unwrappedObj, wrappedKey, rval); +} + +// Handles iterator generation for public jsapi map/set access +template <typename Iter> +bool CallObjFunc(bool (*ObjFunc)(JSContext* cx, Iter kind, HandleObject obj, + MutableHandleValue iter), + JSContext* cx, Iter iterType, HandleObject obj, + MutableHandleValue rval) { + CHECK_THREAD(cx); + cx->check(obj); + + // Always unwrap, in case this is an xray or cross-compartment wrapper. + RootedObject unwrappedObj(cx); + unwrappedObj = UncheckedUnwrap(obj); + { + // Retrieve the iterator while in the unwrapped map/set's compartment, + // otherwise we'll crash on a compartment assert. + JSAutoRealm ar(cx, unwrappedObj); + if (!ObjFunc(cx, iterType, unwrappedObj, rval)) { + return false; + } + } + + // If the caller is in a different compartment than the map/set, rewrap the + // iterator object into the caller's compartment. + if (obj != unwrappedObj) { + if (!JS_WrapValue(cx, rval)) { + return false; + } + } + return true; +} + +/*** JS public APIs *********************************************************/ + +JS_PUBLIC_API JSObject* JS::NewMapObject(JSContext* cx) { + return MapObject::create(cx); +} + +JS_PUBLIC_API uint32_t JS::MapSize(JSContext* cx, HandleObject obj) { + return CallObjFunc<uint32_t>(&MapObject::size, cx, obj); +} + +JS_PUBLIC_API bool JS::MapGet(JSContext* cx, HandleObject obj, HandleValue key, + MutableHandleValue rval) { + CHECK_THREAD(cx); + cx->check(obj, key, rval); + + // Unwrap the object, and enter its realm. If object isn't wrapped, + // this is essentially a noop. + RootedObject unwrappedObj(cx); + unwrappedObj = UncheckedUnwrap(obj); + { + JSAutoRealm ar(cx, unwrappedObj); + RootedValue wrappedKey(cx, key); + + // If we passed in a wrapper, wrap our key into its compartment now. + if (obj != unwrappedObj) { + if (!JS_WrapValue(cx, &wrappedKey)) { + return false; + } + } + if (!MapObject::get(cx, unwrappedObj, wrappedKey, rval)) { + return false; + } + } + + // If we passed in a wrapper, wrap our return value on the way out. + if (obj != unwrappedObj) { + if (!JS_WrapValue(cx, rval)) { + return false; + } + } + return true; +} + +JS_PUBLIC_API bool JS::MapSet(JSContext* cx, HandleObject obj, HandleValue key, + HandleValue val) { + CHECK_THREAD(cx); + cx->check(obj, key, val); + + // Unwrap the object, and enter its compartment. If object isn't wrapped, + // this is essentially a noop. + RootedObject unwrappedObj(cx); + unwrappedObj = UncheckedUnwrap(obj); + { + JSAutoRealm ar(cx, unwrappedObj); + + // If we passed in a wrapper, wrap both key and value before adding to + // the map + RootedValue wrappedKey(cx, key); + RootedValue wrappedValue(cx, val); + if (obj != unwrappedObj) { + if (!JS_WrapValue(cx, &wrappedKey) || !JS_WrapValue(cx, &wrappedValue)) { + return false; + } + } + return MapObject::set(cx, unwrappedObj, wrappedKey, wrappedValue); + } +} + +JS_PUBLIC_API bool JS::MapHas(JSContext* cx, HandleObject obj, HandleValue key, + bool* rval) { + return CallObjFunc(MapObject::has, cx, obj, key, rval); +} + +JS_PUBLIC_API bool JS::MapDelete(JSContext* cx, HandleObject obj, + HandleValue key, bool* rval) { + return CallObjFunc(MapObject::delete_, cx, obj, key, rval); +} + +JS_PUBLIC_API bool JS::MapClear(JSContext* cx, HandleObject obj) { + return CallObjFunc(&MapObject::clear, cx, obj); +} + +JS_PUBLIC_API bool JS::MapKeys(JSContext* cx, HandleObject obj, + MutableHandleValue rval) { + return CallObjFunc(&MapObject::iterator, cx, MapObject::Keys, obj, rval); +} + +JS_PUBLIC_API bool JS::MapValues(JSContext* cx, HandleObject obj, + MutableHandleValue rval) { + return CallObjFunc(&MapObject::iterator, cx, MapObject::Values, obj, rval); +} + +JS_PUBLIC_API bool JS::MapEntries(JSContext* cx, HandleObject obj, + MutableHandleValue rval) { + return CallObjFunc(&MapObject::iterator, cx, MapObject::Entries, obj, rval); +} + +JS_PUBLIC_API bool JS::MapForEach(JSContext* cx, HandleObject obj, + HandleValue callbackFn, HandleValue thisVal) { + return forEach("MapForEach", cx, obj, callbackFn, thisVal); +} + +JS_PUBLIC_API JSObject* JS::NewSetObject(JSContext* cx) { + return SetObject::create(cx); +} + +JS_PUBLIC_API uint32_t JS::SetSize(JSContext* cx, HandleObject obj) { + return CallObjFunc<uint32_t>(&SetObject::size, cx, obj); +} + +JS_PUBLIC_API bool JS::SetAdd(JSContext* cx, HandleObject obj, + HandleValue key) { + CHECK_THREAD(cx); + cx->check(obj, key); + + // Unwrap the object, and enter its compartment. If object isn't wrapped, + // this is essentially a noop. + RootedObject unwrappedObj(cx); + unwrappedObj = UncheckedUnwrap(obj); + { + JSAutoRealm ar(cx, unwrappedObj); + + // If we passed in a wrapper, wrap key before adding to the set + RootedValue wrappedKey(cx, key); + if (obj != unwrappedObj) { + if (!JS_WrapValue(cx, &wrappedKey)) { + return false; + } + } + return SetObject::add(cx, unwrappedObj, wrappedKey); + } +} + +JS_PUBLIC_API bool JS::SetHas(JSContext* cx, HandleObject obj, HandleValue key, + bool* rval) { + return CallObjFunc(SetObject::has, cx, obj, key, rval); +} + +JS_PUBLIC_API bool JS::SetDelete(JSContext* cx, HandleObject obj, + HandleValue key, bool* rval) { + return CallObjFunc(SetObject::delete_, cx, obj, key, rval); +} + +JS_PUBLIC_API bool JS::SetClear(JSContext* cx, HandleObject obj) { + return CallObjFunc(&SetObject::clear, cx, obj); +} + +JS_PUBLIC_API bool JS::SetKeys(JSContext* cx, HandleObject obj, + MutableHandleValue rval) { + return SetValues(cx, obj, rval); +} + +JS_PUBLIC_API bool JS::SetValues(JSContext* cx, HandleObject obj, + MutableHandleValue rval) { + return CallObjFunc(&SetObject::iterator, cx, SetObject::Values, obj, rval); +} + +JS_PUBLIC_API bool JS::SetEntries(JSContext* cx, HandleObject obj, + MutableHandleValue rval) { + return CallObjFunc(&SetObject::iterator, cx, SetObject::Entries, obj, rval); +} + +JS_PUBLIC_API bool JS::SetForEach(JSContext* cx, HandleObject obj, + HandleValue callbackFn, HandleValue thisVal) { + return forEach("SetForEach", cx, obj, callbackFn, thisVal); +} diff --git a/js/src/builtin/MapObject.h b/js/src/builtin/MapObject.h new file mode 100644 index 0000000000..73b623d25f --- /dev/null +++ b/js/src/builtin/MapObject.h @@ -0,0 +1,469 @@ +/* -*- 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 builtin_MapObject_h +#define builtin_MapObject_h + +#include "mozilla/MemoryReporting.h" + +#include "builtin/SelfHostingDefines.h" +#include "vm/JSObject.h" +#include "vm/NativeObject.h" +#include "vm/PIC.h" + +namespace js { + +/* + * Comparing two ropes for equality can fail. The js::HashTable template + * requires infallible hash() and match() operations. Therefore we require + * all values to be converted to hashable form before being used as a key + * in a Map or Set object. + * + * All values except ropes are hashable as-is. + */ +class HashableValue { + Value value; + + public: + HashableValue() : value(UndefinedValue()) {} + explicit HashableValue(JSWhyMagic whyMagic) : value(MagicValue(whyMagic)) {} + + [[nodiscard]] bool setValue(JSContext* cx, HandleValue v); + HashNumber hash(const mozilla::HashCodeScrambler& hcs) const; + + // Value equality. Separate BigInt instances may compare equal. + bool equals(const HashableValue& other) const; + + // Bitwise equality. + bool operator==(const HashableValue& other) const { + return value == other.value; + } + bool operator!=(const HashableValue& other) const { + return !(*this == other); + } + + const Value& get() const { return value; } + operator Value() const { return get(); } + + void trace(JSTracer* trc) { + TraceManuallyBarrieredEdge(trc, &value, "HashableValue"); + } +}; + +template <typename Wrapper> +class WrappedPtrOperations<HashableValue, Wrapper> { + public: + Value get() const { return static_cast<const Wrapper*>(this)->get().get(); } +}; + +template <typename Wrapper> +class MutableWrappedPtrOperations<HashableValue, Wrapper> + : public WrappedPtrOperations<HashableValue, Wrapper> { + public: + [[nodiscard]] bool setValue(JSContext* cx, HandleValue v) { + return static_cast<Wrapper*>(this)->get().setValue(cx, v); + } +}; + +template <> +struct InternalBarrierMethods<HashableValue> { + static bool isMarkable(const HashableValue& v) { return v.get().isGCThing(); } + + static void preBarrier(const HashableValue& v) { + if (isMarkable(v)) { + gc::ValuePreWriteBarrier(v.get()); + } + } + +#ifdef DEBUG + static void assertThingIsNotGray(const HashableValue& v) { + JS::AssertValueIsNotGray(v.get()); + } +#endif +}; + +struct HashableValueHasher { + using Key = PreBarriered<HashableValue>; + using Lookup = HashableValue; + + static HashNumber hash(const Lookup& v, + const mozilla::HashCodeScrambler& hcs) { + return v.hash(hcs); + } + static bool match(const Key& k, const Lookup& l) { return k.get().equals(l); } + static bool isEmpty(const Key& v) { + return v.get().get().isMagic(JS_HASH_KEY_EMPTY); + } + static void makeEmpty(Key* vp) { vp->set(HashableValue(JS_HASH_KEY_EMPTY)); } +}; + +using ValueMap = OrderedHashMap<PreBarriered<HashableValue>, HeapPtr<Value>, + HashableValueHasher, CellAllocPolicy>; + +using ValueSet = OrderedHashSet<PreBarriered<HashableValue>, + HashableValueHasher, CellAllocPolicy>; + +template <typename ObjectT> +class OrderedHashTableRef; + +struct UnbarrieredHashPolicy; + +class MapObject : public NativeObject { + public: + enum IteratorKind { Keys, Values, Entries }; + static_assert( + Keys == ITEM_KIND_KEY, + "IteratorKind Keys must match self-hosting define for item kind key."); + static_assert(Values == ITEM_KIND_VALUE, + "IteratorKind Values must match self-hosting define for item " + "kind value."); + static_assert( + Entries == ITEM_KIND_KEY_AND_VALUE, + "IteratorKind Entries must match self-hosting define for item kind " + "key-and-value."); + + static const JSClass class_; + static const JSClass protoClass_; + + enum { DataSlot, NurseryKeysSlot, HasNurseryMemorySlot, SlotCount }; + + [[nodiscard]] static bool getKeysAndValuesInterleaved( + HandleObject obj, JS::MutableHandle<GCVector<JS::Value>> entries); + [[nodiscard]] static bool entries(JSContext* cx, unsigned argc, Value* vp); + static MapObject* create(JSContext* cx, HandleObject proto = nullptr); + + // Publicly exposed Map calls for JSAPI access (webidl maplike/setlike + // interfaces, etc.) + static uint32_t size(JSContext* cx, HandleObject obj); + [[nodiscard]] static bool get(JSContext* cx, HandleObject obj, + HandleValue key, MutableHandleValue rval); + [[nodiscard]] static bool has(JSContext* cx, HandleObject obj, + HandleValue key, bool* rval); + [[nodiscard]] static bool delete_(JSContext* cx, HandleObject obj, + HandleValue key, bool* rval); + + // Set call for public JSAPI exposure. Does not actually return map object + // as stated in spec, expects caller to return a value. for instance, with + // webidl maplike/setlike, should return interface object. + [[nodiscard]] static bool set(JSContext* cx, HandleObject obj, + HandleValue key, HandleValue val); + [[nodiscard]] static bool clear(JSContext* cx, HandleObject obj); + [[nodiscard]] static bool iterator(JSContext* cx, IteratorKind kind, + HandleObject obj, MutableHandleValue iter); + + // OrderedHashMap with the same memory layout as ValueMap but without wrappers + // that perform post barriers. Used when the owning JS object is in the + // nursery. + using PreBarrieredTable = + OrderedHashMap<PreBarriered<HashableValue>, PreBarriered<Value>, + HashableValueHasher, CellAllocPolicy>; + + // OrderedHashMap with the same memory layout as ValueMap but without any + // wrappers that perform barriers. Used when updating the nursery allocated + // keys map during minor GC. + using UnbarrieredTable = + OrderedHashMap<Value, Value, UnbarrieredHashPolicy, CellAllocPolicy>; + friend class OrderedHashTableRef<MapObject>; + + static void sweepAfterMinorGC(JS::GCContext* gcx, MapObject* mapobj); + + size_t sizeOfData(mozilla::MallocSizeOf mallocSizeOf); + + static constexpr size_t getDataSlotOffset() { + return getFixedSlotOffset(DataSlot); + } + + const ValueMap* getData() { return getTableUnchecked(); } + + [[nodiscard]] static bool get(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool set(JSContext* cx, unsigned argc, Value* vp); + + static bool isOriginalSizeGetter(Native native) { + return native == static_cast<Native>(MapObject::size); + } + + private: + static const ClassSpec classSpec_; + static const JSClassOps classOps_; + + static const JSPropertySpec properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec staticProperties[]; + + PreBarrieredTable* nurseryTable() { + MOZ_ASSERT(IsInsideNursery(this)); + return maybePtrFromReservedSlot<PreBarrieredTable>(DataSlot); + } + ValueMap* tenuredTable() { + MOZ_ASSERT(!IsInsideNursery(this)); + return getTableUnchecked(); + } + ValueMap* getTableUnchecked() { + return maybePtrFromReservedSlot<ValueMap>(DataSlot); + } + + static inline bool setWithHashableKey(JSContext* cx, MapObject* obj, + Handle<HashableValue> key, + Handle<Value> value); + + static bool finishInit(JSContext* cx, HandleObject ctor, HandleObject proto); + + static const ValueMap& extract(HandleObject o); + static const ValueMap& extract(const CallArgs& args); + static void trace(JSTracer* trc, JSObject* obj); + static void finalize(JS::GCContext* gcx, JSObject* obj); + [[nodiscard]] static bool construct(JSContext* cx, unsigned argc, Value* vp); + + static bool is(HandleValue v); + static bool is(HandleObject o); + + [[nodiscard]] static bool iterator_impl(JSContext* cx, const CallArgs& args, + IteratorKind kind); + + [[nodiscard]] static bool size_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool size(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool get_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool has_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool has(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool set_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool delete_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool delete_(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool keys_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool keys(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool values_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool values(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool entries_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool clear_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool clear(JSContext* cx, unsigned argc, Value* vp); +}; + +class MapIteratorObject : public NativeObject { + public: + static const JSClass class_; + + enum { TargetSlot, RangeSlot, KindSlot, SlotCount }; + + static_assert( + TargetSlot == ITERATOR_SLOT_TARGET, + "TargetSlot must match self-hosting define for iterated object slot."); + static_assert( + RangeSlot == ITERATOR_SLOT_RANGE, + "RangeSlot must match self-hosting define for range or index slot."); + static_assert(KindSlot == ITERATOR_SLOT_ITEM_KIND, + "KindSlot must match self-hosting define for item kind slot."); + + static const JSFunctionSpec methods[]; + static MapIteratorObject* create(JSContext* cx, HandleObject mapobj, + const ValueMap* data, + MapObject::IteratorKind kind); + static void finalize(JS::GCContext* gcx, JSObject* obj); + static size_t objectMoved(JSObject* obj, JSObject* old); + + void init(MapObject* mapObj, MapObject::IteratorKind kind) { + initFixedSlot(TargetSlot, JS::ObjectValue(*mapObj)); + initFixedSlot(RangeSlot, JS::PrivateValue(nullptr)); + initFixedSlot(KindSlot, JS::Int32Value(int32_t(kind))); + } + + [[nodiscard]] static bool next(MapIteratorObject* mapIterator, + ArrayObject* resultPairObj); + + static JSObject* createResultPair(JSContext* cx); + + private: + inline MapObject::IteratorKind kind() const; +}; + +class SetObject : public NativeObject { + public: + enum IteratorKind { Keys, Values, Entries }; + + static_assert( + Keys == ITEM_KIND_KEY, + "IteratorKind Keys must match self-hosting define for item kind key."); + static_assert(Values == ITEM_KIND_VALUE, + "IteratorKind Values must match self-hosting define for item " + "kind value."); + static_assert( + Entries == ITEM_KIND_KEY_AND_VALUE, + "IteratorKind Entries must match self-hosting define for item kind " + "key-and-value."); + + static const JSClass class_; + static const JSClass protoClass_; + + enum { DataSlot, NurseryKeysSlot, HasNurseryMemorySlot, SlotCount }; + + [[nodiscard]] static bool keys(JSContext* cx, HandleObject obj, + JS::MutableHandle<GCVector<JS::Value>> keys); + [[nodiscard]] static bool values(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool add(JSContext* cx, HandleObject obj, + HandleValue key); + + // Publicly exposed Set calls for JSAPI access (webidl maplike/setlike + // interfaces, etc.) + static SetObject* create(JSContext* cx, HandleObject proto = nullptr); + static uint32_t size(JSContext* cx, HandleObject obj); + [[nodiscard]] static bool add(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool has(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool has(JSContext* cx, HandleObject obj, + HandleValue key, bool* rval); + [[nodiscard]] static bool clear(JSContext* cx, HandleObject obj); + [[nodiscard]] static bool iterator(JSContext* cx, IteratorKind kind, + HandleObject obj, MutableHandleValue iter); + [[nodiscard]] static bool delete_(JSContext* cx, HandleObject obj, + HandleValue key, bool* rval); + + using UnbarrieredTable = + OrderedHashSet<Value, UnbarrieredHashPolicy, CellAllocPolicy>; + friend class OrderedHashTableRef<SetObject>; + + static void sweepAfterMinorGC(JS::GCContext* gcx, SetObject* setobj); + + size_t sizeOfData(mozilla::MallocSizeOf mallocSizeOf); + + static constexpr size_t getDataSlotOffset() { + return getFixedSlotOffset(DataSlot); + } + + ValueSet* getData() { return getTableUnchecked(); } + + static bool isOriginalSizeGetter(Native native) { + return native == static_cast<Native>(SetObject::size); + } + + private: + static const ClassSpec classSpec_; + static const JSClassOps classOps_; + + static const JSPropertySpec properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec staticProperties[]; + + ValueSet* getTableUnchecked() { + return maybePtrFromReservedSlot<ValueSet>(DataSlot); + } + + static bool finishInit(JSContext* cx, HandleObject ctor, HandleObject proto); + + static ValueSet& extract(HandleObject o); + static ValueSet& extract(const CallArgs& args); + static void trace(JSTracer* trc, JSObject* obj); + static void finalize(JS::GCContext* gcx, JSObject* obj); + static bool construct(JSContext* cx, unsigned argc, Value* vp); + + static bool is(HandleValue v); + static bool is(HandleObject o); + + static bool isBuiltinAdd(HandleValue add); + + [[nodiscard]] static bool iterator_impl(JSContext* cx, const CallArgs& args, + IteratorKind kind); + + [[nodiscard]] static bool size_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool size(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool has_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool add_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool delete_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool delete_(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool values_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool entries_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool entries(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool clear_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool clear(JSContext* cx, unsigned argc, Value* vp); +}; + +class SetIteratorObject : public NativeObject { + public: + static const JSClass class_; + + enum { TargetSlot, RangeSlot, KindSlot, SlotCount }; + + static_assert( + TargetSlot == ITERATOR_SLOT_TARGET, + "TargetSlot must match self-hosting define for iterated object slot."); + static_assert( + RangeSlot == ITERATOR_SLOT_RANGE, + "RangeSlot must match self-hosting define for range or index slot."); + static_assert(KindSlot == ITERATOR_SLOT_ITEM_KIND, + "KindSlot must match self-hosting define for item kind slot."); + + static const JSFunctionSpec methods[]; + static SetIteratorObject* create(JSContext* cx, HandleObject setobj, + ValueSet* data, + SetObject::IteratorKind kind); + static void finalize(JS::GCContext* gcx, JSObject* obj); + static size_t objectMoved(JSObject* obj, JSObject* old); + + void init(SetObject* setObj, SetObject::IteratorKind kind) { + initFixedSlot(TargetSlot, JS::ObjectValue(*setObj)); + initFixedSlot(RangeSlot, JS::PrivateValue(nullptr)); + initFixedSlot(KindSlot, JS::Int32Value(int32_t(kind))); + } + + [[nodiscard]] static bool next(SetIteratorObject* setIterator, + ArrayObject* resultObj); + + static JSObject* createResult(JSContext* cx); + + private: + inline SetObject::IteratorKind kind() const; +}; + +using SetInitGetPrototypeOp = NativeObject* (*)(JSContext*, + Handle<GlobalObject*>); +using SetInitIsBuiltinOp = bool (*)(HandleValue); + +template <SetInitGetPrototypeOp getPrototypeOp, SetInitIsBuiltinOp isBuiltinOp> +[[nodiscard]] static bool IsOptimizableInitForSet(JSContext* cx, + HandleObject setObject, + HandleValue iterable, + bool* optimized) { + MOZ_ASSERT(!*optimized); + + if (!iterable.isObject()) { + return true; + } + + RootedObject array(cx, &iterable.toObject()); + if (!IsPackedArray(array)) { + return true; + } + + // Get the canonical prototype object. + Rooted<NativeObject*> setProto(cx, getPrototypeOp(cx, cx->global())); + if (!setProto) { + return false; + } + + // Ensures setObject's prototype is the canonical prototype. + if (setObject->staticPrototype() != setProto) { + return true; + } + + // Look up the 'add' value on the prototype object. + mozilla::Maybe<PropertyInfo> addProp = setProto->lookup(cx, cx->names().add); + if (addProp.isNothing() || !addProp->isDataProperty()) { + return true; + } + + // Get the referred value, ensure it holds the canonical add function. + RootedValue add(cx, setProto->getSlot(addProp->slot())); + if (!isBuiltinOp(add)) { + return true; + } + + ForOfPIC::Chain* stubChain = ForOfPIC::getOrCreate(cx); + if (!stubChain) { + return false; + } + + return stubChain->tryOptimizeArray(cx, array.as<ArrayObject>(), optimized); +} + +} /* namespace js */ + +#endif /* builtin_MapObject_h */ diff --git a/js/src/builtin/ModuleObject.cpp b/js/src/builtin/ModuleObject.cpp new file mode 100644 index 0000000000..f227728f98 --- /dev/null +++ b/js/src/builtin/ModuleObject.cpp @@ -0,0 +1,2548 @@ +/* -*- 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 "builtin/ModuleObject.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/EnumSet.h" +#include "mozilla/ScopeExit.h" + +#include "builtin/Promise.h" +#include "builtin/SelfHostingDefines.h" +#include "frontend/ParseNode.h" +#include "frontend/ParserAtom.h" // TaggedParserAtomIndex, ParserAtomsTable, ParserAtom +#include "frontend/SharedContext.h" +#include "frontend/Stencil.h" +#include "gc/GCContext.h" +#include "gc/Tracer.h" +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "js/Modules.h" // JS::GetModulePrivate, JS::ModuleDynamicImportHook +#include "vm/EqualityOperations.h" // js::SameValue +#include "vm/Interpreter.h" // Execute, Lambda, ReportRuntimeLexicalError +#include "vm/ModuleBuilder.h" // js::ModuleBuilder +#include "vm/Modules.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/PromiseObject.h" // js::PromiseObject +#include "vm/SharedStencil.h" // js::GCThingIndex + +#include "builtin/HandlerFunction-inl.h" // js::ExtraValueFromHandler, js::NewHandler{,WithExtraValue}, js::TargetFromHandler +#include "gc/GCContext-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/JSScript-inl.h" +#include "vm/List-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::Maybe; +using mozilla::Nothing; +using mozilla::Some; +using mozilla::Span; + +static_assert(ModuleStatus::Unlinked < ModuleStatus::Linking && + ModuleStatus::Linking < ModuleStatus::Linked && + ModuleStatus::Linked < ModuleStatus::Evaluating && + ModuleStatus::Evaluating < ModuleStatus::EvaluatingAsync && + ModuleStatus::EvaluatingAsync < ModuleStatus::Evaluated && + ModuleStatus::Evaluated < ModuleStatus::Evaluated_Error, + "Module statuses are ordered incorrectly"); + +static Value StringOrNullValue(JSString* maybeString) { + return maybeString ? StringValue(maybeString) : NullValue(); +} + +#define DEFINE_ATOM_ACCESSOR_METHOD(cls, name, slot) \ + JSAtom* cls::name() const { \ + Value value = getReservedSlot(slot); \ + return &value.toString()->asAtom(); \ + } + +#define DEFINE_ATOM_OR_NULL_ACCESSOR_METHOD(cls, name, slot) \ + JSAtom* cls::name() const { \ + Value value = getReservedSlot(slot); \ + if (value.isNull()) { \ + return nullptr; \ + } \ + return &value.toString()->asAtom(); \ + } + +#define DEFINE_UINT32_ACCESSOR_METHOD(cls, name, slot) \ + uint32_t cls::name() const { \ + Value value = getReservedSlot(slot); \ + MOZ_ASSERT(value.toNumber() >= 0); \ + if (value.isInt32()) { \ + return value.toInt32(); \ + } \ + return JS::ToUint32(value.toDouble()); \ + } + +/////////////////////////////////////////////////////////////////////////// +// ImportEntry + +ImportEntry::ImportEntry(Handle<ModuleRequestObject*> moduleRequest, + Handle<JSAtom*> maybeImportName, + Handle<JSAtom*> localName, uint32_t lineNumber, + uint32_t columnNumber) + : moduleRequest_(moduleRequest), + importName_(maybeImportName), + localName_(localName), + lineNumber_(lineNumber), + columnNumber_(columnNumber) {} + +void ImportEntry::trace(JSTracer* trc) { + TraceEdge(trc, &moduleRequest_, "ImportEntry::moduleRequest_"); + TraceNullableEdge(trc, &importName_, "ImportEntry::importName_"); + TraceNullableEdge(trc, &localName_, "ImportEntry::localName_"); +} + +/////////////////////////////////////////////////////////////////////////// +// ExportEntry + +ExportEntry::ExportEntry(Handle<JSAtom*> maybeExportName, + Handle<ModuleRequestObject*> moduleRequest, + Handle<JSAtom*> maybeImportName, + Handle<JSAtom*> maybeLocalName, uint32_t lineNumber, + uint32_t columnNumber) + : exportName_(maybeExportName), + moduleRequest_(moduleRequest), + importName_(maybeImportName), + localName_(maybeLocalName), + lineNumber_(lineNumber), + columnNumber_(columnNumber) { + // Line and column numbers are optional for export entries since direct + // entries are checked at parse time. +} + +void ExportEntry::trace(JSTracer* trc) { + TraceNullableEdge(trc, &exportName_, "ExportEntry::exportName_"); + TraceNullableEdge(trc, &moduleRequest_, "ExportEntry::moduleRequest_"); + TraceNullableEdge(trc, &importName_, "ExportEntry::importName_"); + TraceNullableEdge(trc, &localName_, "ExportEntry::localName_"); +} + +/////////////////////////////////////////////////////////////////////////// +// RequestedModule + +/* static */ +RequestedModule::RequestedModule(Handle<ModuleRequestObject*> moduleRequest, + uint32_t lineNumber, uint32_t columnNumber) + : moduleRequest_(moduleRequest), + lineNumber_(lineNumber), + columnNumber_(columnNumber) {} + +void RequestedModule::trace(JSTracer* trc) { + TraceEdge(trc, &moduleRequest_, "ExportEntry::moduleRequest_"); +} + +/////////////////////////////////////////////////////////////////////////// +// ResolvedBindingObject + +/* static */ const JSClass ResolvedBindingObject::class_ = { + "ResolvedBinding", + JSCLASS_HAS_RESERVED_SLOTS(ResolvedBindingObject::SlotCount)}; + +ModuleObject* ResolvedBindingObject::module() const { + Value value = getReservedSlot(ModuleSlot); + return &value.toObject().as<ModuleObject>(); +} + +JSAtom* ResolvedBindingObject::bindingName() const { + Value value = getReservedSlot(BindingNameSlot); + return &value.toString()->asAtom(); +} + +/* static */ +bool ResolvedBindingObject::isInstance(HandleValue value) { + return value.isObject() && value.toObject().is<ResolvedBindingObject>(); +} + +/* static */ +ResolvedBindingObject* ResolvedBindingObject::create( + JSContext* cx, Handle<ModuleObject*> module, Handle<JSAtom*> bindingName) { + ResolvedBindingObject* self = + NewObjectWithGivenProto<ResolvedBindingObject>(cx, nullptr); + if (!self) { + return nullptr; + } + + self->initReservedSlot(ModuleSlot, ObjectValue(*module)); + self->initReservedSlot(BindingNameSlot, StringValue(bindingName)); + return self; +} + +/////////////////////////////////////////////////////////////////////////// +// ModuleRequestObject +/* static */ const JSClass ModuleRequestObject::class_ = { + "ModuleRequest", + JSCLASS_HAS_RESERVED_SLOTS(ModuleRequestObject::SlotCount)}; + +DEFINE_ATOM_OR_NULL_ACCESSOR_METHOD(ModuleRequestObject, specifier, + SpecifierSlot) + +ArrayObject* ModuleRequestObject::assertions() const { + JSObject* obj = getReservedSlot(AssertionSlot).toObjectOrNull(); + if (!obj) { + return nullptr; + } + + return &obj->as<ArrayObject>(); +} + +/* static */ +bool ModuleRequestObject::isInstance(HandleValue value) { + return value.isObject() && value.toObject().is<ModuleRequestObject>(); +} + +/* static */ +ModuleRequestObject* ModuleRequestObject::create( + JSContext* cx, Handle<JSAtom*> specifier, + Handle<ArrayObject*> maybeAssertions) { + ModuleRequestObject* self = + NewObjectWithGivenProto<ModuleRequestObject>(cx, nullptr); + if (!self) { + return nullptr; + } + + self->initReservedSlot(SpecifierSlot, StringOrNullValue(specifier)); + self->initReservedSlot(AssertionSlot, ObjectOrNullValue(maybeAssertions)); + return self; +} + +/////////////////////////////////////////////////////////////////////////// +// IndirectBindingMap + +IndirectBindingMap::Binding::Binding(ModuleEnvironmentObject* environment, + jsid targetName, PropertyInfo prop) + : environment(environment), +#ifdef DEBUG + targetName(targetName), +#endif + prop(prop) { +} + +void IndirectBindingMap::trace(JSTracer* trc) { + if (!map_) { + return; + } + + for (Map::Enum e(*map_); !e.empty(); e.popFront()) { + Binding& b = e.front().value(); + TraceEdge(trc, &b.environment, "module bindings environment"); +#ifdef DEBUG + TraceEdge(trc, &b.targetName, "module bindings target name"); +#endif + mozilla::DebugOnly<jsid> prev(e.front().key()); + TraceEdge(trc, &e.front().mutableKey(), "module bindings binding name"); + MOZ_ASSERT(e.front().key() == prev); + } +} + +bool IndirectBindingMap::put(JSContext* cx, HandleId name, + Handle<ModuleEnvironmentObject*> environment, + HandleId targetName) { + if (!map_) { + map_.emplace(cx->zone()); + } + + mozilla::Maybe<PropertyInfo> prop = environment->lookup(cx, targetName); + MOZ_ASSERT(prop.isSome()); + if (!map_->put(name, Binding(environment, targetName, *prop))) { + ReportOutOfMemory(cx); + return false; + } + + return true; +} + +bool IndirectBindingMap::lookup(jsid name, ModuleEnvironmentObject** envOut, + mozilla::Maybe<PropertyInfo>* propOut) const { + if (!map_) { + return false; + } + + auto ptr = map_->lookup(name); + if (!ptr) { + return false; + } + + const Binding& binding = ptr->value(); + MOZ_ASSERT(binding.environment); + MOZ_ASSERT( + binding.environment->containsPure(binding.targetName, binding.prop)); + *envOut = binding.environment; + *propOut = Some(binding.prop); + return true; +} + +/////////////////////////////////////////////////////////////////////////// +// ModuleNamespaceObject + +/* static */ +const ModuleNamespaceObject::ProxyHandler ModuleNamespaceObject::proxyHandler; + +/* static */ +bool ModuleNamespaceObject::isInstance(HandleValue value) { + return value.isObject() && value.toObject().is<ModuleNamespaceObject>(); +} + +/* static */ +ModuleNamespaceObject* ModuleNamespaceObject::create( + JSContext* cx, Handle<ModuleObject*> module, + MutableHandle<UniquePtr<ExportNameVector>> exports, + MutableHandle<UniquePtr<IndirectBindingMap>> bindings) { + RootedValue priv(cx, ObjectValue(*module)); + ProxyOptions options; + options.setLazyProto(true); + + RootedObject object( + cx, NewProxyObject(cx, &proxyHandler, priv, nullptr, options)); + if (!object) { + return nullptr; + } + + SetProxyReservedSlot(object, ExportsSlot, + PrivateValue(exports.get().release())); + AddCellMemory(object, sizeof(ExportNameVector), MemoryUse::ModuleExports); + + SetProxyReservedSlot(object, BindingsSlot, + PrivateValue(bindings.get().release())); + AddCellMemory(object, sizeof(IndirectBindingMap), + MemoryUse::ModuleBindingMap); + + return &object->as<ModuleNamespaceObject>(); +} + +ModuleObject& ModuleNamespaceObject::module() { + return GetProxyPrivate(this).toObject().as<ModuleObject>(); +} + +const ExportNameVector& ModuleNamespaceObject::exports() const { + Value value = GetProxyReservedSlot(this, ExportsSlot); + auto* exports = static_cast<ExportNameVector*>(value.toPrivate()); + MOZ_ASSERT(exports); + return *exports; +} + +ExportNameVector& ModuleNamespaceObject::mutableExports() { + // Get a non-const reference for tracing/destruction. Do not actually mutate + // this vector! This would be incorrect without adding barriers. + return const_cast<ExportNameVector&>(exports()); +} + +IndirectBindingMap& ModuleNamespaceObject::bindings() { + Value value = GetProxyReservedSlot(this, BindingsSlot); + auto* bindings = static_cast<IndirectBindingMap*>(value.toPrivate()); + MOZ_ASSERT(bindings); + return *bindings; +} + +bool ModuleNamespaceObject::hasExports() const { + // Exports may not be present if we hit OOM in initialization. + return !GetProxyReservedSlot(this, ExportsSlot).isUndefined(); +} + +bool ModuleNamespaceObject::hasBindings() const { + // Import bindings may not be present if we hit OOM in initialization. + return !GetProxyReservedSlot(this, BindingsSlot).isUndefined(); +} + +bool ModuleNamespaceObject::addBinding(JSContext* cx, + Handle<JSAtom*> exportedName, + Handle<ModuleObject*> targetModule, + Handle<JSAtom*> targetName) { + Rooted<ModuleEnvironmentObject*> environment( + cx, &targetModule->initialEnvironment()); + RootedId exportedNameId(cx, AtomToId(exportedName)); + RootedId targetNameId(cx, AtomToId(targetName)); + return bindings().put(cx, exportedNameId, environment, targetNameId); +} + +const char ModuleNamespaceObject::ProxyHandler::family = 0; + +ModuleNamespaceObject::ProxyHandler::ProxyHandler() + : BaseProxyHandler(&family, false) {} + +bool ModuleNamespaceObject::ProxyHandler::getPrototype( + JSContext* cx, HandleObject proxy, MutableHandleObject protop) const { + protop.set(nullptr); + return true; +} + +bool ModuleNamespaceObject::ProxyHandler::setPrototype( + JSContext* cx, HandleObject proxy, HandleObject proto, + ObjectOpResult& result) const { + if (!proto) { + return result.succeed(); + } + return result.failCantSetProto(); +} + +bool ModuleNamespaceObject::ProxyHandler::getPrototypeIfOrdinary( + JSContext* cx, HandleObject proxy, bool* isOrdinary, + MutableHandleObject protop) const { + *isOrdinary = false; + return true; +} + +bool ModuleNamespaceObject::ProxyHandler::setImmutablePrototype( + JSContext* cx, HandleObject proxy, bool* succeeded) const { + *succeeded = true; + return true; +} + +bool ModuleNamespaceObject::ProxyHandler::isExtensible(JSContext* cx, + HandleObject proxy, + bool* extensible) const { + *extensible = false; + return true; +} + +bool ModuleNamespaceObject::ProxyHandler::preventExtensions( + JSContext* cx, HandleObject proxy, ObjectOpResult& result) const { + result.succeed(); + return true; +} + +bool ModuleNamespaceObject::ProxyHandler::getOwnPropertyDescriptor( + JSContext* cx, HandleObject proxy, HandleId id, + MutableHandle<mozilla::Maybe<PropertyDescriptor>> desc) const { + Rooted<ModuleNamespaceObject*> ns(cx, &proxy->as<ModuleNamespaceObject>()); + if (id.isSymbol()) { + if (id.isWellKnownSymbol(JS::SymbolCode::toStringTag)) { + desc.set(Some(PropertyDescriptor::Data(StringValue(cx->names().Module)))); + return true; + } + + desc.reset(); + return true; + } + + const IndirectBindingMap& bindings = ns->bindings(); + ModuleEnvironmentObject* env; + mozilla::Maybe<PropertyInfo> prop; + if (!bindings.lookup(id, &env, &prop)) { + // Not found. + desc.reset(); + return true; + } + + RootedValue value(cx, env->getSlot(prop->slot())); + if (value.isMagic(JS_UNINITIALIZED_LEXICAL)) { + ReportRuntimeLexicalError(cx, JSMSG_UNINITIALIZED_LEXICAL, id); + return false; + } + + desc.set( + Some(PropertyDescriptor::Data(value, {JS::PropertyAttribute::Enumerable, + JS::PropertyAttribute::Writable}))); + return true; +} + +static bool ValidatePropertyDescriptor( + JSContext* cx, Handle<PropertyDescriptor> desc, bool expectedWritable, + bool expectedEnumerable, bool expectedConfigurable, + HandleValue expectedValue, ObjectOpResult& result) { + if (desc.isAccessorDescriptor()) { + return result.fail(JSMSG_CANT_REDEFINE_PROP); + } + + if (desc.hasWritable() && desc.writable() != expectedWritable) { + return result.fail(JSMSG_CANT_REDEFINE_PROP); + } + + if (desc.hasEnumerable() && desc.enumerable() != expectedEnumerable) { + return result.fail(JSMSG_CANT_REDEFINE_PROP); + } + + if (desc.hasConfigurable() && desc.configurable() != expectedConfigurable) { + return result.fail(JSMSG_CANT_REDEFINE_PROP); + } + + if (desc.hasValue()) { + bool same; + if (!SameValue(cx, desc.value(), expectedValue, &same)) { + return false; + } + if (!same) { + return result.fail(JSMSG_CANT_REDEFINE_PROP); + } + } + + return result.succeed(); +} + +bool ModuleNamespaceObject::ProxyHandler::defineProperty( + JSContext* cx, HandleObject proxy, HandleId id, + Handle<PropertyDescriptor> desc, ObjectOpResult& result) const { + if (id.isSymbol()) { + if (id.isWellKnownSymbol(JS::SymbolCode::toStringTag)) { + RootedValue value(cx, StringValue(cx->names().Module)); + return ValidatePropertyDescriptor(cx, desc, false, false, false, value, + result); + } + return result.fail(JSMSG_CANT_DEFINE_PROP_OBJECT_NOT_EXTENSIBLE); + } + + const IndirectBindingMap& bindings = + proxy->as<ModuleNamespaceObject>().bindings(); + ModuleEnvironmentObject* env; + mozilla::Maybe<PropertyInfo> prop; + if (!bindings.lookup(id, &env, &prop)) { + return result.fail(JSMSG_CANT_DEFINE_PROP_OBJECT_NOT_EXTENSIBLE); + } + + RootedValue value(cx, env->getSlot(prop->slot())); + if (value.isMagic(JS_UNINITIALIZED_LEXICAL)) { + ReportRuntimeLexicalError(cx, JSMSG_UNINITIALIZED_LEXICAL, id); + return false; + } + + return ValidatePropertyDescriptor(cx, desc, true, true, false, value, result); +} + +bool ModuleNamespaceObject::ProxyHandler::has(JSContext* cx, HandleObject proxy, + HandleId id, bool* bp) const { + Rooted<ModuleNamespaceObject*> ns(cx, &proxy->as<ModuleNamespaceObject>()); + if (id.isSymbol()) { + *bp = id.isWellKnownSymbol(JS::SymbolCode::toStringTag); + return true; + } + + *bp = ns->bindings().has(id); + return true; +} + +bool ModuleNamespaceObject::ProxyHandler::get(JSContext* cx, HandleObject proxy, + HandleValue receiver, HandleId id, + MutableHandleValue vp) const { + Rooted<ModuleNamespaceObject*> ns(cx, &proxy->as<ModuleNamespaceObject>()); + if (id.isSymbol()) { + if (id.isWellKnownSymbol(JS::SymbolCode::toStringTag)) { + vp.setString(cx->names().Module); + return true; + } + + vp.setUndefined(); + return true; + } + + ModuleEnvironmentObject* env; + mozilla::Maybe<PropertyInfo> prop; + if (!ns->bindings().lookup(id, &env, &prop)) { + vp.setUndefined(); + return true; + } + + RootedValue value(cx, env->getSlot(prop->slot())); + if (value.isMagic(JS_UNINITIALIZED_LEXICAL)) { + ReportRuntimeLexicalError(cx, JSMSG_UNINITIALIZED_LEXICAL, id); + return false; + } + + vp.set(value); + return true; +} + +bool ModuleNamespaceObject::ProxyHandler::set(JSContext* cx, HandleObject proxy, + HandleId id, HandleValue v, + HandleValue receiver, + ObjectOpResult& result) const { + return result.failReadOnly(); +} + +bool ModuleNamespaceObject::ProxyHandler::delete_( + JSContext* cx, HandleObject proxy, HandleId id, + ObjectOpResult& result) const { + Rooted<ModuleNamespaceObject*> ns(cx, &proxy->as<ModuleNamespaceObject>()); + if (id.isSymbol()) { + if (id.isWellKnownSymbol(JS::SymbolCode::toStringTag)) { + return result.failCantDelete(); + } + + return result.succeed(); + } + + if (ns->bindings().has(id)) { + return result.failCantDelete(); + } + + return result.succeed(); +} + +bool ModuleNamespaceObject::ProxyHandler::ownPropertyKeys( + JSContext* cx, HandleObject proxy, MutableHandleIdVector props) const { + Rooted<ModuleNamespaceObject*> ns(cx, &proxy->as<ModuleNamespaceObject>()); + uint32_t count = ns->exports().length(); + if (!props.reserve(props.length() + count + 1)) { + return false; + } + + for (JSAtom* atom : ns->exports()) { + props.infallibleAppend(AtomToId(atom)); + } + props.infallibleAppend( + PropertyKey::Symbol(cx->wellKnownSymbols().toStringTag)); + + return true; +} + +void ModuleNamespaceObject::ProxyHandler::trace(JSTracer* trc, + JSObject* proxy) const { + auto& self = proxy->as<ModuleNamespaceObject>(); + + if (self.hasExports()) { + self.mutableExports().trace(trc); + } + + if (self.hasBindings()) { + self.bindings().trace(trc); + } +} + +void ModuleNamespaceObject::ProxyHandler::finalize(JS::GCContext* gcx, + JSObject* proxy) const { + auto& self = proxy->as<ModuleNamespaceObject>(); + + if (self.hasExports()) { + gcx->delete_(proxy, &self.mutableExports(), MemoryUse::ModuleExports); + } + + if (self.hasBindings()) { + gcx->delete_(proxy, &self.bindings(), MemoryUse::ModuleBindingMap); + } +} + +/////////////////////////////////////////////////////////////////////////// +// CyclicModuleFields + +// The fields of a cyclic module record, as described in: +// https://tc39.es/ecma262/#sec-cyclic-module-records +class js::CyclicModuleFields { + public: + ModuleStatus status = ModuleStatus::Unlinked; + + bool hasTopLevelAwait : 1; + + private: + // Flag bits that determine whether other fields are present. + bool hasDfsIndex : 1; + bool hasDfsAncestorIndex : 1; + bool isAsyncEvaluating : 1; + bool hasPendingAsyncDependencies : 1; + + // Fields whose presence is conditional on the flag bits above. + uint32_t dfsIndex = 0; + uint32_t dfsAncestorIndex = 0; + uint32_t asyncEvaluatingPostOrder = 0; + uint32_t pendingAsyncDependencies = 0; + + // Fields describing the layout of exportEntries. + uint32_t indirectExportEntriesStart = 0; + uint32_t starExportEntriesStart = 0; + + public: + HeapPtr<Value> evaluationError; + HeapPtr<JSObject*> metaObject; + HeapPtr<ScriptSourceObject*> scriptSourceObject; + RequestedModuleVector requestedModules; + ImportEntryVector importEntries; + ExportEntryVector exportEntries; + IndirectBindingMap importBindings; + UniquePtr<FunctionDeclarationVector> functionDeclarations; + HeapPtr<PromiseObject*> topLevelCapability; + HeapPtr<ListObject*> asyncParentModules; + HeapPtr<ModuleObject*> cycleRoot; + + public: + CyclicModuleFields(); + + void trace(JSTracer* trc); + + void initExportEntries(MutableHandle<ExportEntryVector> allEntries, + uint32_t localExportCount, + uint32_t indirectExportCount, + uint32_t starExportCount); + Span<const ExportEntry> localExportEntries() const; + Span<const ExportEntry> indirectExportEntries() const; + Span<const ExportEntry> starExportEntries() const; + + void setDfsIndex(uint32_t index); + Maybe<uint32_t> maybeDfsIndex() const; + void setDfsAncestorIndex(uint32_t index); + Maybe<uint32_t> maybeDfsAncestorIndex() const; + void clearDfsIndexes(); + + void setAsyncEvaluating(uint32_t postOrder); + bool getIsAsyncEvaluating() const; + Maybe<uint32_t> maybeAsyncEvaluatingPostOrder() const; + void clearAsyncEvaluatingPostOrder(); + + void setPendingAsyncDependencies(uint32_t newValue); + Maybe<uint32_t> maybePendingAsyncDependencies() const; +}; + +CyclicModuleFields::CyclicModuleFields() + : hasTopLevelAwait(false), + hasDfsIndex(false), + hasDfsAncestorIndex(false), + isAsyncEvaluating(false), + hasPendingAsyncDependencies(false) {} + +void CyclicModuleFields::trace(JSTracer* trc) { + TraceEdge(trc, &evaluationError, "CyclicModuleFields::evaluationError"); + TraceNullableEdge(trc, &metaObject, "CyclicModuleFields::metaObject"); + TraceNullableEdge(trc, &scriptSourceObject, + "CyclicModuleFields::scriptSourceObject"); + requestedModules.trace(trc); + importEntries.trace(trc); + exportEntries.trace(trc); + importBindings.trace(trc); + TraceNullableEdge(trc, &topLevelCapability, + "CyclicModuleFields::topLevelCapability"); + TraceNullableEdge(trc, &asyncParentModules, + "CyclicModuleFields::asyncParentModules"); + TraceNullableEdge(trc, &cycleRoot, "CyclicModuleFields::cycleRoot"); +} + +void CyclicModuleFields::initExportEntries( + MutableHandle<ExportEntryVector> allEntries, uint32_t localExportCount, + uint32_t indirectExportCount, uint32_t starExportCount) { + MOZ_ASSERT(allEntries.length() == + localExportCount + indirectExportCount + starExportCount); + + exportEntries = std::move(allEntries.get()); + indirectExportEntriesStart = localExportCount; + starExportEntriesStart = indirectExportEntriesStart + indirectExportCount; +} + +Span<const ExportEntry> CyclicModuleFields::localExportEntries() const { + MOZ_ASSERT(indirectExportEntriesStart <= exportEntries.length()); + return Span(exportEntries.begin(), + exportEntries.begin() + indirectExportEntriesStart); +} + +Span<const ExportEntry> CyclicModuleFields::indirectExportEntries() const { + MOZ_ASSERT(indirectExportEntriesStart <= starExportEntriesStart); + MOZ_ASSERT(starExportEntriesStart <= exportEntries.length()); + return Span(exportEntries.begin() + indirectExportEntriesStart, + exportEntries.begin() + starExportEntriesStart); +} + +Span<const ExportEntry> CyclicModuleFields::starExportEntries() const { + MOZ_ASSERT(starExportEntriesStart <= exportEntries.length()); + return Span(exportEntries.begin() + starExportEntriesStart, + exportEntries.end()); +} + +void CyclicModuleFields::setDfsIndex(uint32_t index) { + dfsIndex = index; + hasDfsIndex = true; +} + +Maybe<uint32_t> CyclicModuleFields::maybeDfsIndex() const { + return hasDfsIndex ? Some(dfsIndex) : Nothing(); +} + +void CyclicModuleFields::setDfsAncestorIndex(uint32_t index) { + dfsAncestorIndex = index; + hasDfsAncestorIndex = true; +} + +Maybe<uint32_t> CyclicModuleFields::maybeDfsAncestorIndex() const { + return hasDfsAncestorIndex ? Some(dfsAncestorIndex) : Nothing(); +} + +void CyclicModuleFields::clearDfsIndexes() { + dfsIndex = 0; + hasDfsIndex = false; + dfsAncestorIndex = 0; + hasDfsAncestorIndex = false; +} + +void CyclicModuleFields::setAsyncEvaluating(uint32_t postOrder) { + isAsyncEvaluating = true; + asyncEvaluatingPostOrder = postOrder; +} + +bool CyclicModuleFields::getIsAsyncEvaluating() const { + return isAsyncEvaluating; +} + +Maybe<uint32_t> CyclicModuleFields::maybeAsyncEvaluatingPostOrder() const { + if (!isAsyncEvaluating || + asyncEvaluatingPostOrder == ASYNC_EVALUATING_POST_ORDER_CLEARED) { + return Nothing(); + } + + return Some(asyncEvaluatingPostOrder); +} + +void CyclicModuleFields::clearAsyncEvaluatingPostOrder() { + asyncEvaluatingPostOrder = ASYNC_EVALUATING_POST_ORDER_CLEARED; +} + +void CyclicModuleFields::setPendingAsyncDependencies(uint32_t newValue) { + pendingAsyncDependencies = newValue; + hasPendingAsyncDependencies = true; +} + +Maybe<uint32_t> CyclicModuleFields::maybePendingAsyncDependencies() const { + return hasPendingAsyncDependencies ? Some(pendingAsyncDependencies) + : Nothing(); +} + +/////////////////////////////////////////////////////////////////////////// +// ModuleObject + +/* static */ const JSClassOps ModuleObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + ModuleObject::finalize, // finalize + nullptr, // call + nullptr, // construct + ModuleObject::trace, // trace +}; + +/* static */ const JSClass ModuleObject::class_ = { + "Module", + JSCLASS_HAS_RESERVED_SLOTS(ModuleObject::SlotCount) | + JSCLASS_BACKGROUND_FINALIZE, + &ModuleObject::classOps_}; + +/* static */ +bool ModuleObject::isInstance(HandleValue value) { + return value.isObject() && value.toObject().is<ModuleObject>(); +} + +bool ModuleObject::hasCyclicModuleFields() const { + // This currently only returns false if we GC during initialization. + return !getReservedSlot(CyclicModuleFieldsSlot).isUndefined(); +} + +CyclicModuleFields* ModuleObject::cyclicModuleFields() { + void* ptr = getReservedSlot(CyclicModuleFieldsSlot).toPrivate(); + MOZ_ASSERT(ptr); + return static_cast<CyclicModuleFields*>(ptr); +} +const CyclicModuleFields* ModuleObject::cyclicModuleFields() const { + return const_cast<ModuleObject*>(this)->cyclicModuleFields(); +} + +Span<const RequestedModule> ModuleObject::requestedModules() const { + return cyclicModuleFields()->requestedModules; +} + +Span<const ImportEntry> ModuleObject::importEntries() const { + return cyclicModuleFields()->importEntries; +} + +Span<const ExportEntry> ModuleObject::localExportEntries() const { + return cyclicModuleFields()->localExportEntries(); +} + +Span<const ExportEntry> ModuleObject::indirectExportEntries() const { + return cyclicModuleFields()->indirectExportEntries(); +} + +Span<const ExportEntry> ModuleObject::starExportEntries() const { + return cyclicModuleFields()->starExportEntries(); +} + +void ModuleObject::initFunctionDeclarations( + UniquePtr<FunctionDeclarationVector> decls) { + cyclicModuleFields()->functionDeclarations = std::move(decls); +} + +/* static */ +ModuleObject* ModuleObject::create(JSContext* cx) { + Rooted<UniquePtr<CyclicModuleFields>> fields(cx); + fields = cx->make_unique<CyclicModuleFields>(); + if (!fields) { + return nullptr; + } + + Rooted<ModuleObject*> self( + cx, NewObjectWithGivenProto<ModuleObject>(cx, nullptr)); + if (!self) { + return nullptr; + } + + InitReservedSlot(self, CyclicModuleFieldsSlot, fields.release(), + MemoryUse::ModuleCyclicFields); + + return self; +} + +/* static */ +void ModuleObject::finalize(JS::GCContext* gcx, JSObject* obj) { + ModuleObject* self = &obj->as<ModuleObject>(); + if (self->hasCyclicModuleFields()) { + gcx->delete_(obj, self->cyclicModuleFields(), + MemoryUse::ModuleCyclicFields); + } +} + +ModuleEnvironmentObject& ModuleObject::initialEnvironment() const { + Value value = getReservedSlot(EnvironmentSlot); + return value.toObject().as<ModuleEnvironmentObject>(); +} + +ModuleEnvironmentObject* ModuleObject::environment() const { + // Note that this it's valid to call this even if there was an error + // evaluating the module. + + // According to the spec the environment record is created during linking, but + // we create it earlier than that. + if (status() < ModuleStatus::Linked) { + return nullptr; + } + + return &initialEnvironment(); +} + +IndirectBindingMap& ModuleObject::importBindings() { + return cyclicModuleFields()->importBindings; +} + +ModuleNamespaceObject* ModuleObject::namespace_() { + Value value = getReservedSlot(NamespaceSlot); + if (value.isUndefined()) { + return nullptr; + } + return &value.toObject().as<ModuleNamespaceObject>(); +} + +ScriptSourceObject* ModuleObject::scriptSourceObject() const { + return cyclicModuleFields()->scriptSourceObject; +} + +void ModuleObject::initAsyncSlots(JSContext* cx, bool hasTopLevelAwait, + Handle<ListObject*> asyncParentModules) { + cyclicModuleFields()->hasTopLevelAwait = hasTopLevelAwait; + cyclicModuleFields()->asyncParentModules = asyncParentModules; +} + +static uint32_t NextPostOrder(JSRuntime* rt) { + uint32_t ordinal = rt->moduleAsyncEvaluatingPostOrder; + MOZ_ASSERT(ordinal != ASYNC_EVALUATING_POST_ORDER_CLEARED); + MOZ_ASSERT(ordinal < MAX_UINT32); + rt->moduleAsyncEvaluatingPostOrder++; + return ordinal; +} + +// Reset the runtime's moduleAsyncEvaluatingPostOrder counter when the last +// module that was async evaluating is finished. +// +// The graph is not re-entrant and any future modules will be independent from +// this one. +static void MaybeResetPostOrderCounter(JSRuntime* rt, + uint32_t finishedPostOrder) { + if (rt->moduleAsyncEvaluatingPostOrder == finishedPostOrder + 1) { + rt->moduleAsyncEvaluatingPostOrder = ASYNC_EVALUATING_POST_ORDER_INIT; + } +} + +void ModuleObject::setAsyncEvaluating() { + MOZ_ASSERT(!isAsyncEvaluating()); + uint32_t postOrder = NextPostOrder(runtimeFromMainThread()); + cyclicModuleFields()->setAsyncEvaluating(postOrder); +} + +void ModuleObject::initScriptSlots(HandleScript script) { + MOZ_ASSERT(script); + MOZ_ASSERT(script->sourceObject()); + initReservedSlot(ScriptSlot, PrivateGCThingValue(script)); + cyclicModuleFields()->scriptSourceObject = script->sourceObject(); +} + +void ModuleObject::setInitialEnvironment( + Handle<ModuleEnvironmentObject*> initialEnvironment) { + initReservedSlot(EnvironmentSlot, ObjectValue(*initialEnvironment)); +} + +void ModuleObject::initImportExportData( + MutableHandle<RequestedModuleVector> requestedModules, + MutableHandle<ImportEntryVector> importEntries, + MutableHandle<ExportEntryVector> exportEntries, uint32_t localExportCount, + uint32_t indirectExportCount, uint32_t starExportCount) { + cyclicModuleFields()->requestedModules = std::move(requestedModules.get()); + cyclicModuleFields()->importEntries = std::move(importEntries.get()); + cyclicModuleFields()->initExportEntries(exportEntries, localExportCount, + indirectExportCount, starExportCount); +} + +/* static */ +bool ModuleObject::Freeze(JSContext* cx, Handle<ModuleObject*> self) { + return FreezeObject(cx, self); +} + +#ifdef DEBUG +/* static */ inline bool ModuleObject::AssertFrozen( + JSContext* cx, Handle<ModuleObject*> self) { + bool frozen = false; + if (!TestIntegrityLevel(cx, self, IntegrityLevel::Frozen, &frozen)) { + return false; + } + MOZ_ASSERT(frozen); + + return true; +} +#endif + +JSScript* ModuleObject::maybeScript() const { + Value value = getReservedSlot(ScriptSlot); + if (value.isUndefined()) { + return nullptr; + } + BaseScript* script = value.toGCThing()->as<BaseScript>(); + MOZ_ASSERT(script->hasBytecode(), + "Module scripts should always have bytecode"); + return script->asJSScript(); +} + +JSScript* ModuleObject::script() const { + JSScript* ptr = maybeScript(); + MOZ_RELEASE_ASSERT(ptr); + return ptr; +} + +static inline void AssertValidModuleStatus(ModuleStatus status) { + MOZ_ASSERT(status >= ModuleStatus::Unlinked && + status <= ModuleStatus::Evaluated_Error); +} + +ModuleStatus ModuleObject::status() const { + // TODO: When implementing synthetic module records it may be convenient to + // make this method always return a ModuleStatus::Evaluated for such a module + // so we can assert a module's status without checking which kind it is, even + // though synthetic modules don't have this field according to the spec. + + ModuleStatus status = cyclicModuleFields()->status; + AssertValidModuleStatus(status); + + if (status == ModuleStatus::Evaluated_Error) { + return ModuleStatus::Evaluated; + } + + return status; +} + +void ModuleObject::setStatus(ModuleStatus newStatus) { + AssertValidModuleStatus(newStatus); + + // Note that under OOM conditions we can fail the module linking process even + // after modules have been marked as linked. + MOZ_ASSERT((status() <= ModuleStatus::Linked && + newStatus == ModuleStatus::Unlinked) || + newStatus > status(), + "New module status inconsistent with current status"); + + cyclicModuleFields()->status = newStatus; +} + +bool ModuleObject::hasTopLevelAwait() const { + return cyclicModuleFields()->hasTopLevelAwait; +} + +bool ModuleObject::isAsyncEvaluating() const { + return cyclicModuleFields()->getIsAsyncEvaluating(); +} + +Maybe<uint32_t> ModuleObject::maybeDfsIndex() const { + return cyclicModuleFields()->maybeDfsIndex(); +} + +uint32_t ModuleObject::dfsIndex() const { return maybeDfsIndex().value(); } + +void ModuleObject::setDfsIndex(uint32_t index) { + cyclicModuleFields()->setDfsIndex(index); +} + +Maybe<uint32_t> ModuleObject::maybeDfsAncestorIndex() const { + return cyclicModuleFields()->maybeDfsAncestorIndex(); +} + +uint32_t ModuleObject::dfsAncestorIndex() const { + return maybeDfsAncestorIndex().value(); +} + +void ModuleObject::setDfsAncestorIndex(uint32_t index) { + cyclicModuleFields()->setDfsAncestorIndex(index); +} + +void ModuleObject::clearDfsIndexes() { + cyclicModuleFields()->clearDfsIndexes(); +} + +PromiseObject* ModuleObject::maybeTopLevelCapability() const { + return cyclicModuleFields()->topLevelCapability; +} + +PromiseObject* ModuleObject::topLevelCapability() const { + PromiseObject* capability = maybeTopLevelCapability(); + MOZ_RELEASE_ASSERT(capability); + return capability; +} + +// static +PromiseObject* ModuleObject::createTopLevelCapability( + JSContext* cx, Handle<ModuleObject*> module) { + MOZ_ASSERT(!module->maybeTopLevelCapability()); + + Rooted<PromiseObject*> resultPromise(cx, CreatePromiseObjectForAsync(cx)); + if (!resultPromise) { + return nullptr; + } + + module->setInitialTopLevelCapability(resultPromise); + return resultPromise; +} + +void ModuleObject::setInitialTopLevelCapability( + Handle<PromiseObject*> capability) { + cyclicModuleFields()->topLevelCapability = capability; +} + +ListObject* ModuleObject::asyncParentModules() const { + return cyclicModuleFields()->asyncParentModules; +} + +bool ModuleObject::appendAsyncParentModule(JSContext* cx, + Handle<ModuleObject*> self, + Handle<ModuleObject*> parent) { + Rooted<Value> parentValue(cx, ObjectValue(*parent)); + return self->asyncParentModules()->append(cx, parentValue); +} + +Maybe<uint32_t> ModuleObject::maybePendingAsyncDependencies() const { + return cyclicModuleFields()->maybePendingAsyncDependencies(); +} + +uint32_t ModuleObject::pendingAsyncDependencies() const { + return maybePendingAsyncDependencies().value(); +} + +Maybe<uint32_t> ModuleObject::maybeAsyncEvaluatingPostOrder() const { + return cyclicModuleFields()->maybeAsyncEvaluatingPostOrder(); +} + +uint32_t ModuleObject::getAsyncEvaluatingPostOrder() const { + return cyclicModuleFields()->maybeAsyncEvaluatingPostOrder().value(); +} + +void ModuleObject::clearAsyncEvaluatingPostOrder() { + MOZ_ASSERT(status() == ModuleStatus::Evaluated); + + JSRuntime* rt = runtimeFromMainThread(); + MaybeResetPostOrderCounter(rt, getAsyncEvaluatingPostOrder()); + + cyclicModuleFields()->clearAsyncEvaluatingPostOrder(); +} + +void ModuleObject::setPendingAsyncDependencies(uint32_t newValue) { + cyclicModuleFields()->setPendingAsyncDependencies(newValue); +} + +void ModuleObject::setCycleRoot(ModuleObject* cycleRoot) { + cyclicModuleFields()->cycleRoot = cycleRoot; +} + +ModuleObject* ModuleObject::getCycleRoot() const { + MOZ_RELEASE_ASSERT(cyclicModuleFields()->cycleRoot); + return cyclicModuleFields()->cycleRoot; +} + +bool ModuleObject::hasTopLevelCapability() const { + return cyclicModuleFields()->topLevelCapability; +} + +bool ModuleObject::hadEvaluationError() const { + ModuleStatus fullStatus = cyclicModuleFields()->status; + return fullStatus == ModuleStatus::Evaluated_Error; +} + +void ModuleObject::setEvaluationError(HandleValue newValue) { + MOZ_ASSERT(status() != ModuleStatus::Unlinked); + MOZ_ASSERT(!hadEvaluationError()); + + cyclicModuleFields()->status = ModuleStatus::Evaluated_Error; + cyclicModuleFields()->evaluationError = newValue; + + MOZ_ASSERT(status() == ModuleStatus::Evaluated); + MOZ_ASSERT(hadEvaluationError()); +} + +Value ModuleObject::maybeEvaluationError() const { + return cyclicModuleFields()->evaluationError; +} + +Value ModuleObject::evaluationError() const { + MOZ_ASSERT(hadEvaluationError()); + return maybeEvaluationError(); +} + +JSObject* ModuleObject::metaObject() const { + return cyclicModuleFields()->metaObject; +} + +void ModuleObject::setMetaObject(JSObject* obj) { + MOZ_ASSERT(obj); + MOZ_ASSERT(!metaObject()); + cyclicModuleFields()->metaObject = obj; +} + +/* static */ +void ModuleObject::trace(JSTracer* trc, JSObject* obj) { + ModuleObject& module = obj->as<ModuleObject>(); + if (module.hasCyclicModuleFields()) { + module.cyclicModuleFields()->trace(trc); + } +} + +/* static */ +bool ModuleObject::instantiateFunctionDeclarations(JSContext* cx, + Handle<ModuleObject*> self) { +#ifdef DEBUG + MOZ_ASSERT(self->status() == ModuleStatus::Linking); + if (!AssertFrozen(cx, self)) { + return false; + } +#endif + // |self| initially manages this vector. + UniquePtr<FunctionDeclarationVector>& funDecls = + self->cyclicModuleFields()->functionDeclarations; + if (!funDecls) { + JS_ReportErrorASCII( + cx, "Module function declarations have already been instantiated"); + return false; + } + + Rooted<ModuleEnvironmentObject*> env(cx, &self->initialEnvironment()); + RootedObject obj(cx); + RootedValue value(cx); + RootedFunction fun(cx); + Rooted<PropertyName*> name(cx); + + for (GCThingIndex funIndex : *funDecls) { + fun.set(self->script()->getFunction(funIndex)); + obj = Lambda(cx, fun, env); + if (!obj) { + return false; + } + + name = fun->explicitName()->asPropertyName(); + value = ObjectValue(*obj); + if (!SetProperty(cx, env, name, value)) { + return false; + } + } + + // Free the vector, now its contents are no longer needed. + funDecls.reset(); + + return true; +} + +/* static */ +bool ModuleObject::execute(JSContext* cx, Handle<ModuleObject*> self) { +#ifdef DEBUG + MOZ_ASSERT(self->status() == ModuleStatus::Evaluating || + self->status() == ModuleStatus::EvaluatingAsync || + self->status() == ModuleStatus::Evaluated); + MOZ_ASSERT(!self->hadEvaluationError()); + if (!AssertFrozen(cx, self)) { + return false; + } +#endif + + RootedScript script(cx, self->script()); + + auto guardA = mozilla::MakeScopeExit([&] { + if (self->hasTopLevelAwait()) { + // Handled in AsyncModuleExecutionFulfilled and + // AsyncModuleExecutionRejected. + return; + } + ModuleObject::onTopLevelEvaluationFinished(self); + }); + + Rooted<ModuleEnvironmentObject*> env(cx, self->environment()); + if (!env) { + JS_ReportErrorASCII(cx, + "Module declarations have not yet been instantiated"); + return false; + } + + Rooted<Value> ignored(cx); + return Execute(cx, script, env, &ignored); +} + +/* static */ +void ModuleObject::onTopLevelEvaluationFinished(ModuleObject* module) { + // ScriptSlot is used by debugger to access environments during evaluating + // the top-level script. + // Clear the reference at exit to prevent us keeping this alive unnecessarily. + module->setReservedSlot(ScriptSlot, UndefinedValue()); +} + +/* static */ +ModuleNamespaceObject* ModuleObject::createNamespace( + JSContext* cx, Handle<ModuleObject*> self, + MutableHandle<UniquePtr<ExportNameVector>> exports) { + MOZ_ASSERT(!self->namespace_()); + + Rooted<UniquePtr<IndirectBindingMap>> bindings(cx); + bindings = cx->make_unique<IndirectBindingMap>(); + if (!bindings) { + return nullptr; + } + + auto* ns = ModuleNamespaceObject::create(cx, self, exports, &bindings); + if (!ns) { + return nullptr; + } + + self->initReservedSlot(NamespaceSlot, ObjectValue(*ns)); + return ns; +} + +/* static */ +bool ModuleObject::createEnvironment(JSContext* cx, + Handle<ModuleObject*> self) { + Rooted<ModuleEnvironmentObject*> env( + cx, ModuleEnvironmentObject::create(cx, self)); + if (!env) { + return false; + } + + self->setInitialEnvironment(env); + return true; +} + +/////////////////////////////////////////////////////////////////////////// +// ModuleBuilder + +ModuleBuilder::ModuleBuilder(FrontendContext* fc, + const frontend::EitherParser& eitherParser) + : fc_(fc), + eitherParser_(eitherParser), + requestedModuleSpecifiers_(fc), + importEntries_(fc), + exportEntries_(fc), + exportNames_(fc) {} + +bool ModuleBuilder::noteFunctionDeclaration(FrontendContext* fc, + uint32_t funIndex) { + if (!functionDecls_.emplaceBack(funIndex)) { + js::ReportOutOfMemory(fc); + return false; + } + return true; +} + +void ModuleBuilder::noteAsync(frontend::StencilModuleMetadata& metadata) { + metadata.isAsync = true; +} + +bool ModuleBuilder::buildTables(frontend::StencilModuleMetadata& metadata) { + // https://tc39.es/ecma262/#sec-parsemodule + // 15.2.1.17.1 ParseModule, Steps 4-11. + + // Step 4. + metadata.moduleRequests = std::move(moduleRequests_); + metadata.requestedModules = std::move(requestedModules_); + + // Step 5. + if (!metadata.importEntries.reserve(importEntries_.count())) { + js::ReportOutOfMemory(fc_); + return false; + } + for (auto r = importEntries_.all(); !r.empty(); r.popFront()) { + frontend::StencilModuleEntry& entry = r.front().value(); + metadata.importEntries.infallibleAppend(entry); + } + + // Steps 6-11. + for (const frontend::StencilModuleEntry& exp : exportEntries_) { + if (!exp.moduleRequest) { + frontend::StencilModuleEntry* importEntry = importEntryFor(exp.localName); + if (!importEntry) { + if (!metadata.localExportEntries.append(exp)) { + js::ReportOutOfMemory(fc_); + return false; + } + } else { + if (!importEntry->importName) { + if (!metadata.localExportEntries.append(exp)) { + js::ReportOutOfMemory(fc_); + return false; + } + } else { + // All names should have already been marked as used-by-stencil. + auto entry = frontend::StencilModuleEntry::exportFromEntry( + importEntry->moduleRequest, importEntry->importName, + exp.exportName, exp.lineno, exp.column); + if (!metadata.indirectExportEntries.append(entry)) { + js::ReportOutOfMemory(fc_); + return false; + } + } + } + } else if (!exp.importName && !exp.exportName) { + if (!metadata.starExportEntries.append(exp)) { + js::ReportOutOfMemory(fc_); + return false; + } + } else { + if (!metadata.indirectExportEntries.append(exp)) { + js::ReportOutOfMemory(fc_); + return false; + } + } + } + + return true; +} + +void ModuleBuilder::finishFunctionDecls( + frontend::StencilModuleMetadata& metadata) { + metadata.functionDecls = std::move(functionDecls_); +} + +bool frontend::StencilModuleMetadata::createModuleRequestObjects( + JSContext* cx, CompilationAtomCache& atomCache, + MutableHandle<ModuleRequestVector> output) const { + if (!output.reserve(moduleRequests.length())) { + ReportOutOfMemory(cx); + return false; + } + + Rooted<ModuleRequestObject*> object(cx); + for (const StencilModuleRequest& request : moduleRequests) { + object = createModuleRequestObject(cx, atomCache, request); + if (!object) { + return false; + } + + output.infallibleEmplaceBack(object); + } + + return true; +} + +ModuleRequestObject* frontend::StencilModuleMetadata::createModuleRequestObject( + JSContext* cx, CompilationAtomCache& atomCache, + const StencilModuleRequest& request) const { + Rooted<ArrayObject*> assertionArray(cx); + uint32_t numberOfAssertions = request.assertions.length(); + if (numberOfAssertions > 0) { + assertionArray = NewDenseFullyAllocatedArray(cx, numberOfAssertions); + if (!assertionArray) { + return nullptr; + } + assertionArray->ensureDenseInitializedLength(0, numberOfAssertions); + + Rooted<PlainObject*> assertionObject(cx); + RootedId assertionKey(cx); + RootedValue assertionValue(cx); + for (uint32_t j = 0; j < numberOfAssertions; ++j) { + assertionObject = NewPlainObject(cx); + if (!assertionObject) { + return nullptr; + } + + JSAtom* jsatom = + atomCache.getExistingAtomAt(cx, request.assertions[j].key); + MOZ_ASSERT(jsatom); + assertionKey = AtomToId(jsatom); + + jsatom = atomCache.getExistingAtomAt(cx, request.assertions[j].value); + MOZ_ASSERT(jsatom); + assertionValue = StringValue(jsatom); + + if (!DefineDataProperty(cx, assertionObject, assertionKey, assertionValue, + JSPROP_ENUMERATE)) { + return nullptr; + } + + assertionArray->initDenseElement(j, ObjectValue(*assertionObject)); + } + } + + Rooted<JSAtom*> specifier(cx, + atomCache.getExistingAtomAt(cx, request.specifier)); + MOZ_ASSERT(specifier); + + return ModuleRequestObject::create(cx, specifier, assertionArray); +} + +bool frontend::StencilModuleMetadata::createImportEntries( + JSContext* cx, CompilationAtomCache& atomCache, + Handle<ModuleRequestVector> moduleRequests, + MutableHandle<ImportEntryVector> output) const { + if (!output.reserve(importEntries.length())) { + ReportOutOfMemory(cx); + return false; + } + + for (const StencilModuleEntry& entry : importEntries) { + Rooted<ModuleRequestObject*> moduleRequest(cx); + moduleRequest = moduleRequests[entry.moduleRequest.value()].get(); + MOZ_ASSERT(moduleRequest); + + Rooted<JSAtom*> localName(cx); + if (entry.localName) { + localName = atomCache.getExistingAtomAt(cx, entry.localName); + MOZ_ASSERT(localName); + } + + Rooted<JSAtom*> importName(cx); + if (entry.importName) { + importName = atomCache.getExistingAtomAt(cx, entry.importName); + MOZ_ASSERT(importName); + } + + MOZ_ASSERT(!entry.exportName); + + output.infallibleEmplaceBack(moduleRequest, importName, localName, + entry.lineno, entry.column); + } + + return true; +} + +bool frontend::StencilModuleMetadata::createExportEntries( + JSContext* cx, frontend::CompilationAtomCache& atomCache, + Handle<ModuleRequestVector> moduleRequests, + const frontend::StencilModuleMetadata::EntryVector& input, + MutableHandle<ExportEntryVector> output) const { + if (!output.reserve(output.length() + input.length())) { + ReportOutOfMemory(cx); + return false; + } + + for (const frontend::StencilModuleEntry& entry : input) { + Rooted<JSAtom*> exportName(cx); + if (entry.exportName) { + exportName = atomCache.getExistingAtomAt(cx, entry.exportName); + MOZ_ASSERT(exportName); + } + + Rooted<ModuleRequestObject*> moduleRequestObject(cx); + if (entry.moduleRequest) { + moduleRequestObject = moduleRequests[entry.moduleRequest.value()].get(); + MOZ_ASSERT(moduleRequestObject); + } + + Rooted<JSAtom*> localName(cx); + if (entry.localName) { + localName = atomCache.getExistingAtomAt(cx, entry.localName); + MOZ_ASSERT(localName); + } + + Rooted<JSAtom*> importName(cx); + if (entry.importName) { + importName = atomCache.getExistingAtomAt(cx, entry.importName); + MOZ_ASSERT(importName); + } + + output.infallibleEmplaceBack(exportName, moduleRequestObject, importName, + localName, entry.lineno, entry.column); + } + + return true; +} + +bool frontend::StencilModuleMetadata::createRequestedModules( + JSContext* cx, CompilationAtomCache& atomCache, + Handle<ModuleRequestVector> moduleRequests, + MutableHandle<RequestedModuleVector> output) const { + if (!output.reserve(requestedModules.length())) { + ReportOutOfMemory(cx); + return false; + } + + for (const frontend::StencilModuleEntry& entry : requestedModules) { + Rooted<ModuleRequestObject*> moduleRequest(cx); + moduleRequest = moduleRequests[entry.moduleRequest.value()].get(); + MOZ_ASSERT(moduleRequest); + + MOZ_ASSERT(!entry.localName); + MOZ_ASSERT(!entry.importName); + MOZ_ASSERT(!entry.exportName); + + output.infallibleEmplaceBack(moduleRequest, entry.lineno, entry.column); + } + + return true; +} + +// Use StencilModuleMetadata data to fill in ModuleObject +bool frontend::StencilModuleMetadata::initModule( + JSContext* cx, FrontendContext* fc, + frontend::CompilationAtomCache& atomCache, + JS::Handle<ModuleObject*> module) const { + Rooted<ModuleRequestVector> moduleRequestsVector(cx); + if (!createModuleRequestObjects(cx, atomCache, &moduleRequestsVector)) { + return false; + } + + Rooted<RequestedModuleVector> requestedModulesVector(cx); + if (!createRequestedModules(cx, atomCache, moduleRequestsVector, + &requestedModulesVector)) { + return false; + } + + Rooted<ImportEntryVector> importEntriesVector(cx); + if (!createImportEntries(cx, atomCache, moduleRequestsVector, + &importEntriesVector)) { + return false; + } + + Rooted<ExportEntryVector> exportEntriesVector(cx); + if (!createExportEntries(cx, atomCache, moduleRequestsVector, + localExportEntries, &exportEntriesVector)) { + return false; + } + + Rooted<ExportEntryVector> indirectExportEntriesVector(cx); + if (!createExportEntries(cx, atomCache, moduleRequestsVector, + indirectExportEntries, &exportEntriesVector)) { + return false; + } + + Rooted<ExportEntryVector> starExportEntriesVector(cx); + if (!createExportEntries(cx, atomCache, moduleRequestsVector, + starExportEntries, &exportEntriesVector)) { + return false; + } + + // Copy the vector of declarations to the ModuleObject. + auto functionDeclsCopy = MakeUnique<FunctionDeclarationVector>(); + if (!functionDeclsCopy || !functionDeclsCopy->appendAll(functionDecls)) { + js::ReportOutOfMemory(fc); + return false; + } + module->initFunctionDeclarations(std::move(functionDeclsCopy)); + + Rooted<ListObject*> asyncParentModulesList(cx, ListObject::create(cx)); + if (!asyncParentModulesList) { + return false; + } + + module->initAsyncSlots(cx, isAsync, asyncParentModulesList); + + module->initImportExportData( + &requestedModulesVector, &importEntriesVector, &exportEntriesVector, + localExportEntries.length(), indirectExportEntries.length(), + starExportEntries.length()); + + return true; +} + +bool ModuleBuilder::isAssertionSupported(JS::ImportAssertion supportedAssertion, + frontend::TaggedParserAtomIndex key) { + if (!key.isWellKnownAtomId()) { + return false; + } + + bool result = false; + + switch (supportedAssertion) { + case JS::ImportAssertion::Type: + result = key.toWellKnownAtomId() == WellKnownAtomId::type; + break; + } + + return result; +} + +bool ModuleBuilder::processAssertions(frontend::StencilModuleRequest& request, + frontend::ListNode* assertionList) { + using namespace js::frontend; + + for (ParseNode* assertionItem : assertionList->contents()) { + BinaryNode* assertion = &assertionItem->as<BinaryNode>(); + MOZ_ASSERT(assertion->isKind(ParseNodeKind::ImportAssertion)); + + auto key = assertion->left()->as<NameNode>().atom(); + auto value = assertion->right()->as<NameNode>().atom(); + + for (JS::ImportAssertion assertion : fc_->getSupportedImportAssertions()) { + if (isAssertionSupported(assertion, key)) { + markUsedByStencil(key); + markUsedByStencil(value); + + StencilModuleAssertion assertionStencil(key, value); + if (!request.assertions.append(assertionStencil)) { + js::ReportOutOfMemory(fc_); + return false; + } + } + } + } + + return true; +} + +bool ModuleBuilder::processImport(frontend::BinaryNode* importNode) { + using namespace js::frontend; + + MOZ_ASSERT(importNode->isKind(ParseNodeKind::ImportDecl)); + + auto* specList = &importNode->left()->as<ListNode>(); + MOZ_ASSERT(specList->isKind(ParseNodeKind::ImportSpecList)); + + auto* moduleRequest = &importNode->right()->as<BinaryNode>(); + MOZ_ASSERT(moduleRequest->isKind(ParseNodeKind::ImportModuleRequest)); + + auto* moduleSpec = &moduleRequest->left()->as<NameNode>(); + MOZ_ASSERT(moduleSpec->isKind(ParseNodeKind::StringExpr)); + + auto* assertionList = &moduleRequest->right()->as<ListNode>(); + MOZ_ASSERT(assertionList->isKind(ParseNodeKind::ImportAssertionList)); + + auto specifier = moduleSpec->atom(); + MaybeModuleRequestIndex moduleRequestIndex = + appendModuleRequest(specifier, assertionList); + if (!moduleRequestIndex.isSome()) { + return false; + } + + if (!maybeAppendRequestedModule(moduleRequestIndex, moduleSpec)) { + return false; + } + + for (ParseNode* item : specList->contents()) { + uint32_t line; + uint32_t column; + eitherParser_.computeLineAndColumn(item->pn_pos.begin, &line, &column); + + StencilModuleEntry entry; + TaggedParserAtomIndex localName; + if (item->isKind(ParseNodeKind::ImportSpec)) { + auto* spec = &item->as<BinaryNode>(); + + auto* importNameNode = &spec->left()->as<NameNode>(); + auto* localNameNode = &spec->right()->as<NameNode>(); + + auto importName = importNameNode->atom(); + localName = localNameNode->atom(); + + markUsedByStencil(localName); + markUsedByStencil(importName); + entry = StencilModuleEntry::importEntry(moduleRequestIndex, localName, + importName, line, column); + } else { + MOZ_ASSERT(item->isKind(ParseNodeKind::ImportNamespaceSpec)); + auto* spec = &item->as<UnaryNode>(); + + auto* localNameNode = &spec->kid()->as<NameNode>(); + + localName = localNameNode->atom(); + + markUsedByStencil(localName); + entry = StencilModuleEntry::importNamespaceEntry(moduleRequestIndex, + localName, line, column); + } + + if (!importEntries_.put(localName, entry)) { + return false; + } + } + + return true; +} + +bool ModuleBuilder::processExport(frontend::ParseNode* exportNode) { + using namespace js::frontend; + + MOZ_ASSERT(exportNode->isKind(ParseNodeKind::ExportStmt) || + exportNode->isKind(ParseNodeKind::ExportDefaultStmt)); + + bool isDefault = exportNode->isKind(ParseNodeKind::ExportDefaultStmt); + ParseNode* kid = isDefault ? exportNode->as<BinaryNode>().left() + : exportNode->as<UnaryNode>().kid(); + + if (isDefault && exportNode->as<BinaryNode>().right()) { + // This is an export default containing an expression. + auto localName = TaggedParserAtomIndex::WellKnown::default_(); + auto exportName = TaggedParserAtomIndex::WellKnown::default_(); + return appendExportEntry(exportName, localName); + } + + switch (kid->getKind()) { + case ParseNodeKind::ExportSpecList: { + MOZ_ASSERT(!isDefault); + for (ParseNode* item : kid->as<ListNode>().contents()) { + BinaryNode* spec = &item->as<BinaryNode>(); + MOZ_ASSERT(spec->isKind(ParseNodeKind::ExportSpec)); + + NameNode* localNameNode = &spec->left()->as<NameNode>(); + NameNode* exportNameNode = &spec->right()->as<NameNode>(); + + auto localName = localNameNode->atom(); + auto exportName = exportNameNode->atom(); + + if (!appendExportEntry(exportName, localName, spec)) { + return false; + } + } + break; + } + + case ParseNodeKind::ClassDecl: { + const ClassNode& cls = kid->as<ClassNode>(); + MOZ_ASSERT(cls.names()); + auto localName = cls.names()->innerBinding()->atom(); + auto exportName = + isDefault ? TaggedParserAtomIndex::WellKnown::default_() : localName; + if (!appendExportEntry(exportName, localName)) { + return false; + } + break; + } + + case ParseNodeKind::VarStmt: + case ParseNodeKind::ConstDecl: + case ParseNodeKind::LetDecl: { + for (ParseNode* binding : kid->as<ListNode>().contents()) { + if (binding->isKind(ParseNodeKind::AssignExpr)) { + binding = binding->as<AssignmentNode>().left(); + } else { + MOZ_ASSERT(binding->isKind(ParseNodeKind::Name)); + } + + if (binding->isKind(ParseNodeKind::Name)) { + auto localName = binding->as<NameNode>().atom(); + auto exportName = isDefault + ? TaggedParserAtomIndex::WellKnown::default_() + : localName; + if (!appendExportEntry(exportName, localName)) { + return false; + } + } else if (binding->isKind(ParseNodeKind::ArrayExpr)) { + if (!processExportArrayBinding(&binding->as<ListNode>())) { + return false; + } + } else { + MOZ_ASSERT(binding->isKind(ParseNodeKind::ObjectExpr)); + if (!processExportObjectBinding(&binding->as<ListNode>())) { + return false; + } + } + } + break; + } + + case ParseNodeKind::Function: { + FunctionBox* box = kid->as<FunctionNode>().funbox(); + MOZ_ASSERT(!box->isArrow()); + auto localName = box->explicitName(); + auto exportName = + isDefault ? TaggedParserAtomIndex::WellKnown::default_() : localName; + if (!appendExportEntry(exportName, localName)) { + return false; + } + break; + } + + default: + MOZ_CRASH("Unexpected parse node"); + } + + return true; +} + +bool ModuleBuilder::processExportBinding(frontend::ParseNode* binding) { + using namespace js::frontend; + + if (binding->isKind(ParseNodeKind::Name)) { + auto name = binding->as<NameNode>().atom(); + return appendExportEntry(name, name); + } + + if (binding->isKind(ParseNodeKind::ArrayExpr)) { + return processExportArrayBinding(&binding->as<ListNode>()); + } + + MOZ_ASSERT(binding->isKind(ParseNodeKind::ObjectExpr)); + return processExportObjectBinding(&binding->as<ListNode>()); +} + +bool ModuleBuilder::processExportArrayBinding(frontend::ListNode* array) { + using namespace js::frontend; + + MOZ_ASSERT(array->isKind(ParseNodeKind::ArrayExpr)); + + for (ParseNode* node : array->contents()) { + if (node->isKind(ParseNodeKind::Elision)) { + continue; + } + + if (node->isKind(ParseNodeKind::Spread)) { + node = node->as<UnaryNode>().kid(); + } else if (node->isKind(ParseNodeKind::AssignExpr)) { + node = node->as<AssignmentNode>().left(); + } + + if (!processExportBinding(node)) { + return false; + } + } + + return true; +} + +bool ModuleBuilder::processExportObjectBinding(frontend::ListNode* obj) { + using namespace js::frontend; + + MOZ_ASSERT(obj->isKind(ParseNodeKind::ObjectExpr)); + + for (ParseNode* node : obj->contents()) { + MOZ_ASSERT(node->isKind(ParseNodeKind::MutateProto) || + node->isKind(ParseNodeKind::PropertyDefinition) || + node->isKind(ParseNodeKind::Shorthand) || + node->isKind(ParseNodeKind::Spread)); + + ParseNode* target; + if (node->isKind(ParseNodeKind::Spread)) { + target = node->as<UnaryNode>().kid(); + } else { + if (node->isKind(ParseNodeKind::MutateProto)) { + target = node->as<UnaryNode>().kid(); + } else { + target = node->as<BinaryNode>().right(); + } + + if (target->isKind(ParseNodeKind::AssignExpr)) { + target = target->as<AssignmentNode>().left(); + } + } + + if (!processExportBinding(target)) { + return false; + } + } + + return true; +} + +bool ModuleBuilder::processExportFrom(frontend::BinaryNode* exportNode) { + using namespace js::frontend; + + MOZ_ASSERT(exportNode->isKind(ParseNodeKind::ExportFromStmt)); + + auto* specList = &exportNode->left()->as<ListNode>(); + MOZ_ASSERT(specList->isKind(ParseNodeKind::ExportSpecList)); + + auto* moduleRequest = &exportNode->right()->as<BinaryNode>(); + MOZ_ASSERT(moduleRequest->isKind(ParseNodeKind::ImportModuleRequest)); + + auto* moduleSpec = &moduleRequest->left()->as<NameNode>(); + MOZ_ASSERT(moduleSpec->isKind(ParseNodeKind::StringExpr)); + + auto* assertionList = &moduleRequest->right()->as<ListNode>(); + MOZ_ASSERT(assertionList->isKind(ParseNodeKind::ImportAssertionList)); + + auto specifier = moduleSpec->atom(); + MaybeModuleRequestIndex moduleRequestIndex = + appendModuleRequest(specifier, assertionList); + if (!moduleRequestIndex.isSome()) { + return false; + } + + if (!maybeAppendRequestedModule(moduleRequestIndex, moduleSpec)) { + return false; + } + + for (ParseNode* spec : specList->contents()) { + uint32_t line; + uint32_t column; + eitherParser_.computeLineAndColumn(spec->pn_pos.begin, &line, &column); + + StencilModuleEntry entry; + TaggedParserAtomIndex exportName; + if (spec->isKind(ParseNodeKind::ExportSpec)) { + auto* importNameNode = &spec->as<BinaryNode>().left()->as<NameNode>(); + auto* exportNameNode = &spec->as<BinaryNode>().right()->as<NameNode>(); + + auto importName = importNameNode->atom(); + exportName = exportNameNode->atom(); + + markUsedByStencil(importName); + markUsedByStencil(exportName); + entry = StencilModuleEntry::exportFromEntry( + moduleRequestIndex, importName, exportName, line, column); + } else if (spec->isKind(ParseNodeKind::ExportNamespaceSpec)) { + auto* exportNameNode = &spec->as<UnaryNode>().kid()->as<NameNode>(); + + exportName = exportNameNode->atom(); + + markUsedByStencil(exportName); + entry = StencilModuleEntry::exportNamespaceFromEntry( + moduleRequestIndex, exportName, line, column); + } else { + MOZ_ASSERT(spec->isKind(ParseNodeKind::ExportBatchSpecStmt)); + + entry = StencilModuleEntry::exportBatchFromEntry(moduleRequestIndex, line, + column); + } + + if (!exportEntries_.append(entry)) { + return false; + } + if (exportName && !exportNames_.put(exportName)) { + return false; + } + } + + return true; +} + +frontend::StencilModuleEntry* ModuleBuilder::importEntryFor( + frontend::TaggedParserAtomIndex localName) const { + MOZ_ASSERT(localName); + auto ptr = importEntries_.lookup(localName); + if (!ptr) { + return nullptr; + } + + return &ptr->value(); +} + +bool ModuleBuilder::hasExportedName( + frontend::TaggedParserAtomIndex name) const { + MOZ_ASSERT(name); + return exportNames_.has(name); +} + +bool ModuleBuilder::appendExportEntry( + frontend::TaggedParserAtomIndex exportName, + frontend::TaggedParserAtomIndex localName, frontend::ParseNode* node) { + uint32_t line = 0; + uint32_t column = 0; + if (node) { + eitherParser_.computeLineAndColumn(node->pn_pos.begin, &line, &column); + } + + markUsedByStencil(localName); + markUsedByStencil(exportName); + auto entry = frontend::StencilModuleEntry::exportAsEntry( + localName, exportName, line, column); + if (!exportEntries_.append(entry)) { + return false; + } + + if (!exportNames_.put(exportName)) { + return false; + } + + return true; +} + +frontend::MaybeModuleRequestIndex ModuleBuilder::appendModuleRequest( + frontend::TaggedParserAtomIndex specifier, + frontend::ListNode* assertionList) { + markUsedByStencil(specifier); + auto request = frontend::StencilModuleRequest(specifier); + + if (!processAssertions(request, assertionList)) { + return MaybeModuleRequestIndex(); + } + + uint32_t index = moduleRequests_.length(); + if (!moduleRequests_.append(request)) { + js::ReportOutOfMemory(fc_); + return MaybeModuleRequestIndex(); + } + + return MaybeModuleRequestIndex(index); +} + +bool ModuleBuilder::maybeAppendRequestedModule( + MaybeModuleRequestIndex moduleRequest, frontend::ParseNode* node) { + auto specifier = moduleRequests_[moduleRequest.value()].specifier; + if (requestedModuleSpecifiers_.has(specifier)) { + return true; + } + + uint32_t line; + uint32_t column; + eitherParser_.computeLineAndColumn(node->pn_pos.begin, &line, &column); + + auto entry = frontend::StencilModuleEntry::requestedModule(moduleRequest, + line, column); + + if (!requestedModules_.append(entry)) { + js::ReportOutOfMemory(fc_); + return false; + } + + return requestedModuleSpecifiers_.put(specifier); +} + +void ModuleBuilder::markUsedByStencil(frontend::TaggedParserAtomIndex name) { + // Imported/exported identifiers must be atomized. + eitherParser_.parserAtoms().markUsedByStencil( + name, frontend::ParserAtom::Atomize::Yes); +} + +JSObject* js::GetOrCreateModuleMetaObject(JSContext* cx, + HandleObject moduleArg) { + Handle<ModuleObject*> module = moduleArg.as<ModuleObject>(); + if (JSObject* obj = module->metaObject()) { + return obj; + } + + RootedObject metaObject(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!metaObject) { + return nullptr; + } + + JS::ModuleMetadataHook func = cx->runtime()->moduleMetadataHook; + if (!func) { + JS_ReportErrorASCII(cx, "Module metadata hook not set"); + return nullptr; + } + + RootedValue modulePrivate(cx, JS::GetModulePrivate(module)); + if (!func(cx, modulePrivate, metaObject)) { + return nullptr; + } + + module->setMetaObject(metaObject); + + return metaObject; +} + +ModuleObject* js::CallModuleResolveHook(JSContext* cx, + HandleValue referencingPrivate, + HandleObject moduleRequest) { + JS::ModuleResolveHook moduleResolveHook = cx->runtime()->moduleResolveHook; + if (!moduleResolveHook) { + JS_ReportErrorASCII(cx, "Module resolve hook not set"); + return nullptr; + } + + RootedObject result(cx, + moduleResolveHook(cx, referencingPrivate, moduleRequest)); + if (!result) { + return nullptr; + } + + if (!result->is<ModuleObject>()) { + JS_ReportErrorASCII(cx, "Module resolve hook did not return Module object"); + return nullptr; + } + + return &result->as<ModuleObject>(); +} + +bool ModuleObject::topLevelCapabilityResolve(JSContext* cx, + Handle<ModuleObject*> module) { + RootedValue rval(cx); + Rooted<PromiseObject*> promise( + cx, &module->topLevelCapability()->as<PromiseObject>()); + return AsyncFunctionReturned(cx, promise, rval); +} + +bool ModuleObject::topLevelCapabilityReject(JSContext* cx, + Handle<ModuleObject*> module, + HandleValue error) { + Rooted<PromiseObject*> promise( + cx, &module->topLevelCapability()->as<PromiseObject>()); + return AsyncFunctionThrown(cx, promise, error); +} + +// https://tc39.es/proposal-import-assertions/#sec-evaluate-import-call +// NOTE: The caller needs to handle the promise. +static bool EvaluateDynamicImportOptions( + JSContext* cx, HandleValue optionsArg, + MutableHandle<ArrayObject*> assertionArrayArg) { + // Step 10. If options is not undefined, then. + if (optionsArg.isUndefined()) { + return true; + } + + // Step 10.a. If Type(options) is not Object, + if (!optionsArg.isObject()) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, "import", + "object or undefined", InformalValueTypeName(optionsArg)); + return false; + } + + RootedObject assertWrapperObject(cx, &optionsArg.toObject()); + RootedValue assertValue(cx); + + // Step 10.b. Let assertionsObj be Get(options, "assert"). + RootedId assertId(cx, NameToId(cx->names().assert_)); + if (!GetProperty(cx, assertWrapperObject, assertWrapperObject, assertId, + &assertValue)) { + return false; + } + + // Step 10.d. If assertionsObj is not undefined. + if (assertValue.isUndefined()) { + return true; + } + + // Step 10.d.i. If Type(assertionsObj) is not Object. + if (!assertValue.isObject()) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, "import", + "object or undefined", InformalValueTypeName(assertValue)); + return false; + } + + // Step 10.d.i. Let keys be EnumerableOwnPropertyNames(assertionsObj, key). + RootedObject assertObject(cx, &assertValue.toObject()); + RootedIdVector assertions(cx); + if (!GetPropertyKeys(cx, assertObject, JSITER_OWNONLY, &assertions)) { + return false; + } + + uint32_t numberOfAssertions = assertions.length(); + if (numberOfAssertions == 0) { + return true; + } + + // Step 9 (reordered). Let assertions be a new empty List. + Rooted<ArrayObject*> assertionArray( + cx, NewDenseFullyAllocatedArray(cx, numberOfAssertions)); + if (!assertionArray) { + return false; + } + assertionArray->ensureDenseInitializedLength(0, numberOfAssertions); + + // Step 10.d.iv. Let supportedAssertions be + // !HostGetSupportedImportAssertions(). + const JS::ImportAssertionVector& supportedAssertions = + cx->runtime()->supportedImportAssertions; + + size_t numberOfValidAssertions = 0; + + // Step 10.d.v. For each String key of keys, + RootedId key(cx); + for (size_t i = 0; i < numberOfAssertions; i++) { + key = assertions[i]; + + // Step 10.d.v.1. Let value be Get(assertionsObj, key). + RootedValue value(cx); + if (!GetProperty(cx, assertObject, assertObject, key, &value)) { + return false; + } + + // Step 10.d.v.3. If Type(value) is not String, then. + if (!value.isString()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, "import", "string", + InformalValueTypeName(value)); + return false; + } + + // Step 10.d.v.4. If supportedAssertions contains key, then Append { + // [[Key]]: key, [[Value]]: value } to assertions. + for (JS::ImportAssertion assertion : supportedAssertions) { + bool supported = false; + switch (assertion) { + case JS::ImportAssertion::Type: { + supported = key.toAtom() == cx->names().type; + } break; + } + + if (supported) { + Rooted<PlainObject*> assertionObj(cx, NewPlainObject(cx)); + if (!assertionObj) { + return false; + } + + if (!DefineDataProperty(cx, assertionObj, key, value, + JSPROP_ENUMERATE)) { + return false; + } + + assertionArray->initDenseElement(numberOfValidAssertions, + ObjectValue(*assertionObj)); + ++numberOfValidAssertions; + } + } + } + + if (numberOfValidAssertions == 0) { + return true; + } + + assertionArray->setLength(numberOfValidAssertions); + assertionArrayArg.set(assertionArray); + + return true; +} + +JSObject* js::StartDynamicModuleImport(JSContext* cx, HandleScript script, + HandleValue specifierArg, + HandleValue optionsArg) { + RootedObject promiseConstructor(cx, JS::GetPromiseConstructor(cx)); + if (!promiseConstructor) { + return nullptr; + } + + RootedObject promiseObject(cx, JS::NewPromiseObject(cx, nullptr)); + if (!promiseObject) { + return nullptr; + } + + Handle<PromiseObject*> promise = promiseObject.as<PromiseObject>(); + + JS::ModuleDynamicImportHook importHook = + cx->runtime()->moduleDynamicImportHook; + + if (!importHook) { + // Dynamic import can be disabled by a pref and is not supported in all + // contexts (e.g. web workers). + JS_ReportErrorASCII( + cx, + "Dynamic module import is disabled or not supported in this context"); + if (!RejectPromiseWithPendingError(cx, promise)) { + return nullptr; + } + return promise; + } + + RootedString specifier(cx, ToString(cx, specifierArg)); + if (!specifier) { + if (!RejectPromiseWithPendingError(cx, promise)) { + return nullptr; + } + return promise; + } + + RootedValue referencingPrivate(cx, script->sourceObject()->getPrivate()); + cx->runtime()->addRefScriptPrivate(referencingPrivate); + + Rooted<JSAtom*> specifierAtom(cx, AtomizeString(cx, specifier)); + if (!specifierAtom) { + if (!RejectPromiseWithPendingError(cx, promise)) { + return nullptr; + } + return promise; + } + + Rooted<ArrayObject*> assertionArray(cx); + if (!EvaluateDynamicImportOptions(cx, optionsArg, &assertionArray)) { + if (!RejectPromiseWithPendingError(cx, promise)) { + return nullptr; + } + return promise; + } + + RootedObject moduleRequest( + cx, ModuleRequestObject::create(cx, specifierAtom, assertionArray)); + if (!moduleRequest) { + if (!RejectPromiseWithPendingError(cx, promise)) { + return nullptr; + } + return promise; + } + + if (!importHook(cx, referencingPrivate, moduleRequest, promise)) { + cx->runtime()->releaseScriptPrivate(referencingPrivate); + + // If there's no exception pending then the script is terminating + // anyway, so just return nullptr. + if (!cx->isExceptionPending() || + !RejectPromiseWithPendingError(cx, promise)) { + return nullptr; + } + return promise; + } + + return promise; +} + +static bool OnRootModuleRejected(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue error = args.get(0); + + ReportExceptionClosure reportExn(error); + PrepareScriptEnvironmentAndInvoke(cx, cx->global(), reportExn); + + args.rval().setUndefined(); + return true; +}; + +bool js::OnModuleEvaluationFailure(JSContext* cx, + HandleObject evaluationPromise, + JS::ModuleErrorBehaviour errorBehaviour) { + if (evaluationPromise == nullptr) { + return false; + } + + // To allow module evaluation to happen synchronously throw the error + // immediately. This assumes that any error will already have caused the + // promise to be rejected, and doesn't support top-level await. + if (errorBehaviour == JS::ThrowModuleErrorsSync) { + JS::PromiseState state = JS::GetPromiseState(evaluationPromise); + MOZ_DIAGNOSTIC_ASSERT(state == JS::PromiseState::Rejected || + state == JS::PromiseState::Fulfilled); + + JS::SetSettledPromiseIsHandled(cx, evaluationPromise); + if (state == JS::PromiseState::Fulfilled) { + return true; + } + + RootedValue error(cx, JS::GetPromiseResult(evaluationPromise)); + JS_SetPendingException(cx, error); + return false; + } + + RootedFunction onRejected( + cx, NewHandler(cx, OnRootModuleRejected, evaluationPromise)); + if (!onRejected) { + return false; + } + + return JS::AddPromiseReactions(cx, evaluationPromise, nullptr, onRejected); +} + +// Adjustment for Top-level await; +// See: https://github.com/tc39/proposal-dynamic-import/pull/71/files +static bool OnResolvedDynamicModule(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.get(0).isUndefined()); + + // This is a hack to allow us to have the 2 extra variables needed + // for FinishDynamicModuleImport in the resolve callback. + Rooted<ListObject*> resolvedModuleParams(cx, + ExtraFromHandler<ListObject>(args)); + MOZ_ASSERT(resolvedModuleParams->length() == 2); + RootedValue referencingPrivate(cx, resolvedModuleParams->get(0)); + + Rooted<JSAtom*> specifier( + cx, AtomizeString(cx, resolvedModuleParams->get(1).toString())); + if (!specifier) { + return false; + } + + Rooted<PromiseObject*> promise(cx, TargetFromHandler<PromiseObject>(args)); + + auto releasePrivate = mozilla::MakeScopeExit( + [&] { cx->runtime()->releaseScriptPrivate(referencingPrivate); }); + + RootedObject moduleRequest( + cx, ModuleRequestObject::create(cx, specifier, nullptr)); + if (!moduleRequest) { + return RejectPromiseWithPendingError(cx, promise); + } + + RootedObject result( + cx, CallModuleResolveHook(cx, referencingPrivate, moduleRequest)); + + if (!result) { + return RejectPromiseWithPendingError(cx, promise); + } + + Rooted<ModuleObject*> module(cx, &result->as<ModuleObject>()); + if (module->status() != ModuleStatus::EvaluatingAsync && + module->status() != ModuleStatus::Evaluated) { + JS_ReportErrorASCII( + cx, "Unevaluated or errored module returned by module resolve hook"); + return RejectPromiseWithPendingError(cx, promise); + } + + MOZ_ASSERT(module->getCycleRoot() + ->topLevelCapability() + ->as<PromiseObject>() + .state() == JS::PromiseState::Fulfilled); + + RootedObject ns(cx, GetOrCreateModuleNamespace(cx, module)); + if (!ns) { + return RejectPromiseWithPendingError(cx, promise); + } + + args.rval().setUndefined(); + RootedValue value(cx, ObjectValue(*ns)); + return PromiseObject::resolve(cx, promise, value); +}; + +static bool OnRejectedDynamicModule(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue error = args.get(0); + + RootedValue referencingPrivate(cx, ExtraValueFromHandler(args)); + Rooted<PromiseObject*> promise(cx, TargetFromHandler<PromiseObject>(args)); + + auto releasePrivate = mozilla::MakeScopeExit( + [&] { cx->runtime()->releaseScriptPrivate(referencingPrivate); }); + + args.rval().setUndefined(); + return PromiseObject::reject(cx, promise, error); +}; + +bool FinishDynamicModuleImport_impl(JSContext* cx, + HandleObject evaluationPromise, + HandleValue referencingPrivate, + HandleObject moduleRequest, + HandleObject promiseArg) { + Rooted<ListObject*> resolutionArgs(cx, ListObject::create(cx)); + if (!resolutionArgs->append(cx, referencingPrivate)) { + return false; + } + Rooted<Value> stringValue( + cx, StringValue(moduleRequest->as<ModuleRequestObject>().specifier())); + if (!resolutionArgs->append(cx, stringValue)) { + return false; + } + + Rooted<Value> resolutionArgsValue(cx, ObjectValue(*resolutionArgs)); + + RootedFunction onResolved( + cx, NewHandlerWithExtraValue(cx, OnResolvedDynamicModule, promiseArg, + resolutionArgsValue)); + if (!onResolved) { + return false; + } + + RootedFunction onRejected( + cx, NewHandlerWithExtraValue(cx, OnRejectedDynamicModule, promiseArg, + referencingPrivate)); + if (!onRejected) { + return false; + } + + return JS::AddPromiseReactionsIgnoringUnhandledRejection( + cx, evaluationPromise, onResolved, onRejected); +} + +bool js::FinishDynamicModuleImport(JSContext* cx, + HandleObject evaluationPromise, + HandleValue referencingPrivate, + HandleObject moduleRequest, + HandleObject promiseArg) { + // If we do not have an evaluation promise or a module request for the module, + // we can assume that evaluation has failed or been interrupted -- we can + // reject the dynamic module. + auto releasePrivate = mozilla::MakeScopeExit( + [&] { cx->runtime()->releaseScriptPrivate(referencingPrivate); }); + + if (!evaluationPromise || !moduleRequest) { + Handle<PromiseObject*> promise = promiseArg.as<PromiseObject>(); + return RejectPromiseWithPendingError(cx, promise); + } + + if (!FinishDynamicModuleImport_impl(cx, evaluationPromise, referencingPrivate, + moduleRequest, promiseArg)) { + return false; + } + + releasePrivate.release(); + return true; +} diff --git a/js/src/builtin/ModuleObject.h b/js/src/builtin/ModuleObject.h new file mode 100644 index 0000000000..aefabb11af --- /dev/null +++ b/js/src/builtin/ModuleObject.h @@ -0,0 +1,443 @@ +/* -*- 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 builtin_ModuleObject_h +#define builtin_ModuleObject_h + +#include "mozilla/HashTable.h" // mozilla::{HashMap, DefaultHasher} +#include "mozilla/Maybe.h" // mozilla::Maybe +#include "mozilla/Span.h" + +#include <stddef.h> // size_t +#include <stdint.h> // int32_t, uint32_t + +#include "gc/Barrier.h" // HeapPtr +#include "gc/ZoneAllocator.h" // CellAllocPolicy +#include "js/Class.h" // JSClass, ObjectOpResult +#include "js/GCVector.h" +#include "js/Id.h" // jsid +#include "js/Modules.h" +#include "js/Proxy.h" // BaseProxyHandler +#include "js/RootingAPI.h" // Rooted, Handle, MutableHandle +#include "js/TypeDecls.h" // HandleValue, HandleId, HandleObject, HandleScript, MutableHandleValue, MutableHandleIdVector, MutableHandleObject +#include "js/UniquePtr.h" // UniquePtr +#include "vm/JSObject.h" // JSObject +#include "vm/NativeObject.h" // NativeObject +#include "vm/ProxyObject.h" // ProxyObject +#include "vm/SharedStencil.h" // FunctionDeclarationVector + +class JSAtom; +class JSScript; +class JSTracer; + +namespace JS { +class PropertyDescriptor; +class Value; +} // namespace JS + +namespace js { + +class ArrayObject; +class CyclicModuleFields; +class ListObject; +class ModuleEnvironmentObject; +class ModuleObject; +class PromiseObject; +class ScriptSourceObject; + +class ModuleRequestObject : public NativeObject { + public: + enum { SpecifierSlot = 0, AssertionSlot, SlotCount }; + + static const JSClass class_; + static bool isInstance(HandleValue value); + [[nodiscard]] static ModuleRequestObject* create( + JSContext* cx, Handle<JSAtom*> specifier, + Handle<ArrayObject*> maybeAssertions); + + JSAtom* specifier() const; + ArrayObject* assertions() const; +}; + +using ModuleRequestVector = + GCVector<HeapPtr<ModuleRequestObject*>, 0, SystemAllocPolicy>; + +class ImportEntry { + const HeapPtr<ModuleRequestObject*> moduleRequest_; + const HeapPtr<JSAtom*> importName_; + const HeapPtr<JSAtom*> localName_; + const uint32_t lineNumber_; + const uint32_t columnNumber_; + + public: + ImportEntry(Handle<ModuleRequestObject*> moduleRequest, + Handle<JSAtom*> maybeImportName, Handle<JSAtom*> localName, + uint32_t lineNumber, uint32_t columnNumber); + + ModuleRequestObject* moduleRequest() const { return moduleRequest_; } + JSAtom* importName() const { return importName_; } + JSAtom* localName() const { return localName_; } + uint32_t lineNumber() const { return lineNumber_; } + uint32_t columnNumber() const { return columnNumber_; } + + void trace(JSTracer* trc); +}; + +using ImportEntryVector = GCVector<ImportEntry, 0, SystemAllocPolicy>; + +class ExportEntry { + const HeapPtr<JSAtom*> exportName_; + const HeapPtr<ModuleRequestObject*> moduleRequest_; + const HeapPtr<JSAtom*> importName_; + const HeapPtr<JSAtom*> localName_; + const uint32_t lineNumber_; + const uint32_t columnNumber_; + + public: + ExportEntry(Handle<JSAtom*> maybeExportName, + Handle<ModuleRequestObject*> maybeModuleRequest, + Handle<JSAtom*> maybeImportName, Handle<JSAtom*> maybeLocalName, + uint32_t lineNumber, uint32_t columnNumber); + JSAtom* exportName() const { return exportName_; } + ModuleRequestObject* moduleRequest() const { return moduleRequest_; } + JSAtom* importName() const { return importName_; } + JSAtom* localName() const { return localName_; } + uint32_t lineNumber() const { return lineNumber_; } + uint32_t columnNumber() const { return columnNumber_; } + + void trace(JSTracer* trc); +}; + +using ExportEntryVector = GCVector<ExportEntry, 0, SystemAllocPolicy>; + +class RequestedModule { + const HeapPtr<ModuleRequestObject*> moduleRequest_; + const uint32_t lineNumber_; + const uint32_t columnNumber_; + + public: + RequestedModule(Handle<ModuleRequestObject*> moduleRequest, + uint32_t lineNumber, uint32_t columnNumber); + ModuleRequestObject* moduleRequest() const { return moduleRequest_; } + uint32_t lineNumber() const { return lineNumber_; } + uint32_t columnNumber() const { return columnNumber_; } + + void trace(JSTracer* trc); +}; + +using RequestedModuleVector = GCVector<RequestedModule, 0, SystemAllocPolicy>; + +class ResolvedBindingObject : public NativeObject { + public: + enum { ModuleSlot = 0, BindingNameSlot, SlotCount }; + + static const JSClass class_; + static bool isInstance(HandleValue value); + static ResolvedBindingObject* create(JSContext* cx, + Handle<ModuleObject*> module, + Handle<JSAtom*> bindingName); + ModuleObject* module() const; + JSAtom* bindingName() const; +}; + +class IndirectBindingMap { + public: + void trace(JSTracer* trc); + + bool put(JSContext* cx, HandleId name, + Handle<ModuleEnvironmentObject*> environment, HandleId targetName); + + size_t count() const { return map_ ? map_->count() : 0; } + + bool has(jsid name) const { return map_ ? map_->has(name) : false; } + + bool lookup(jsid name, ModuleEnvironmentObject** envOut, + mozilla::Maybe<PropertyInfo>* propOut) const; + + template <typename Func> + void forEachExportedName(Func func) const { + if (!map_) { + return; + } + + for (auto r = map_->all(); !r.empty(); r.popFront()) { + func(r.front().key()); + } + } + + private: + struct Binding { + Binding(ModuleEnvironmentObject* environment, jsid targetName, + PropertyInfo prop); + HeapPtr<ModuleEnvironmentObject*> environment; +#ifdef DEBUG + HeapPtr<jsid> targetName; +#endif + PropertyInfo prop; + }; + + using Map = mozilla::HashMap<PreBarriered<jsid>, Binding, + mozilla::DefaultHasher<PreBarriered<jsid>>, + CellAllocPolicy>; + + mozilla::Maybe<Map> map_; +}; + +// Vector of atoms representing the names exported from a module namespace. +// +// This is used both on the stack and in the heap. +using ExportNameVector = GCVector<HeapPtr<JSAtom*>, 0, SystemAllocPolicy>; + +class ModuleNamespaceObject : public ProxyObject { + public: + enum ModuleNamespaceSlot { ExportsSlot = 0, BindingsSlot }; + + static bool isInstance(HandleValue value); + static ModuleNamespaceObject* create( + JSContext* cx, Handle<ModuleObject*> module, + MutableHandle<UniquePtr<ExportNameVector>> exports, + MutableHandle<UniquePtr<IndirectBindingMap>> bindings); + + ModuleObject& module(); + const ExportNameVector& exports() const; + IndirectBindingMap& bindings(); + + bool addBinding(JSContext* cx, Handle<JSAtom*> exportedName, + Handle<ModuleObject*> targetModule, + Handle<JSAtom*> targetName); + + private: + struct ProxyHandler : public BaseProxyHandler { + ProxyHandler(); + + bool getOwnPropertyDescriptor( + JSContext* cx, HandleObject proxy, HandleId id, + MutableHandle<mozilla::Maybe<PropertyDescriptor>> desc) const override; + bool defineProperty(JSContext* cx, HandleObject proxy, HandleId id, + Handle<PropertyDescriptor> desc, + ObjectOpResult& result) const override; + bool ownPropertyKeys(JSContext* cx, HandleObject proxy, + MutableHandleIdVector props) const override; + bool delete_(JSContext* cx, HandleObject proxy, HandleId id, + ObjectOpResult& result) const override; + bool getPrototype(JSContext* cx, HandleObject proxy, + MutableHandleObject protop) const override; + bool setPrototype(JSContext* cx, HandleObject proxy, HandleObject proto, + ObjectOpResult& result) const override; + bool getPrototypeIfOrdinary(JSContext* cx, HandleObject proxy, + bool* isOrdinary, + MutableHandleObject protop) const override; + bool setImmutablePrototype(JSContext* cx, HandleObject proxy, + bool* succeeded) const override; + + bool preventExtensions(JSContext* cx, HandleObject proxy, + ObjectOpResult& result) const override; + bool isExtensible(JSContext* cx, HandleObject proxy, + bool* extensible) const override; + bool has(JSContext* cx, HandleObject proxy, HandleId id, + bool* bp) const override; + bool get(JSContext* cx, HandleObject proxy, HandleValue receiver, + HandleId id, MutableHandleValue vp) const override; + bool set(JSContext* cx, HandleObject proxy, HandleId id, HandleValue v, + HandleValue receiver, ObjectOpResult& result) const override; + + void trace(JSTracer* trc, JSObject* proxy) const override; + void finalize(JS::GCContext* gcx, JSObject* proxy) const override; + + static const char family; + }; + + bool hasBindings() const; + bool hasExports() const; + + ExportNameVector& mutableExports(); + + public: + static const ProxyHandler proxyHandler; +}; + +// Value types of [[Status]] in a Cyclic Module Record +// https://tc39.es/ecma262/#table-cyclic-module-fields +enum class ModuleStatus : int8_t { + Unlinked, + Linking, + Linked, + Evaluating, + EvaluatingAsync, + Evaluated, + + // Sub-state of Evaluated with error value set. + // + // This is not returned from ModuleObject::status(); use hadEvaluationError() + // to check this. + Evaluated_Error +}; + +// Special values for CyclicModuleFields' asyncEvaluatingPostOrderSlot field, +// which is used as part of the implementation of the AsyncEvaluation field of +// cyclic module records. +// +// The spec requires us to be able to tell the order in which the field was set +// to true for async evaluating modules. +// +// This is arranged by using an integer to record the order. After evaluation is +// complete the value is set to ASYNC_EVALUATING_POST_ORDER_CLEARED. +// +// See https://tc39.es/ecma262/#sec-cyclic-module-records for field defintion. +// See https://tc39.es/ecma262/#sec-async-module-execution-fulfilled for sort +// requirement. + +// Initial value for the runtime's counter used to generate these values. +constexpr uint32_t ASYNC_EVALUATING_POST_ORDER_INIT = 1; + +// Value that the field is set to after being cleared. +constexpr uint32_t ASYNC_EVALUATING_POST_ORDER_CLEARED = 0; + +class ModuleObject : public NativeObject { + public: + // Module fields including those for AbstractModuleRecords described by: + // https://tc39.es/ecma262/#sec-abstract-module-records + enum ModuleSlot { + ScriptSlot = 0, + EnvironmentSlot, + NamespaceSlot, + CyclicModuleFieldsSlot, + SlotCount + }; + + static const JSClass class_; + + static bool isInstance(HandleValue value); + + static ModuleObject* create(JSContext* cx); + + // Initialize the slots on this object that are dependent on the script. + void initScriptSlots(HandleScript script); + + void setInitialEnvironment( + Handle<ModuleEnvironmentObject*> initialEnvironment); + + void initFunctionDeclarations(UniquePtr<FunctionDeclarationVector> decls); + void initImportExportData( + MutableHandle<RequestedModuleVector> requestedModules, + MutableHandle<ImportEntryVector> importEntries, + MutableHandle<ExportEntryVector> exportEntries, uint32_t localExportCount, + uint32_t indirectExportCount, uint32_t starExportCount); + static bool Freeze(JSContext* cx, Handle<ModuleObject*> self); +#ifdef DEBUG + static bool AssertFrozen(JSContext* cx, Handle<ModuleObject*> self); +#endif + + JSScript* maybeScript() const; + JSScript* script() const; + ModuleEnvironmentObject& initialEnvironment() const; + ModuleEnvironmentObject* environment() const; + ModuleNamespaceObject* namespace_(); + ModuleStatus status() const; + mozilla::Maybe<uint32_t> maybeDfsIndex() const; + uint32_t dfsIndex() const; + mozilla::Maybe<uint32_t> maybeDfsAncestorIndex() const; + uint32_t dfsAncestorIndex() const; + bool hadEvaluationError() const; + Value maybeEvaluationError() const; + Value evaluationError() const; + JSObject* metaObject() const; + ScriptSourceObject* scriptSourceObject() const; + mozilla::Span<const RequestedModule> requestedModules() const; + mozilla::Span<const ImportEntry> importEntries() const; + mozilla::Span<const ExportEntry> localExportEntries() const; + mozilla::Span<const ExportEntry> indirectExportEntries() const; + mozilla::Span<const ExportEntry> starExportEntries() const; + IndirectBindingMap& importBindings(); + + void setStatus(ModuleStatus newStatus); + void setDfsIndex(uint32_t index); + void setDfsAncestorIndex(uint32_t index); + void clearDfsIndexes(); + + static PromiseObject* createTopLevelCapability(JSContext* cx, + Handle<ModuleObject*> module); + bool hasTopLevelAwait() const; + bool isAsyncEvaluating() const; + void setAsyncEvaluating(); + void setEvaluationError(HandleValue newValue); + void setPendingAsyncDependencies(uint32_t newValue); + void setInitialTopLevelCapability(Handle<PromiseObject*> capability); + bool hasTopLevelCapability() const; + PromiseObject* maybeTopLevelCapability() const; + PromiseObject* topLevelCapability() const; + ListObject* asyncParentModules() const; + mozilla::Maybe<uint32_t> maybePendingAsyncDependencies() const; + uint32_t pendingAsyncDependencies() const; + mozilla::Maybe<uint32_t> maybeAsyncEvaluatingPostOrder() const; + uint32_t getAsyncEvaluatingPostOrder() const; + void clearAsyncEvaluatingPostOrder(); + void setCycleRoot(ModuleObject* cycleRoot); + ModuleObject* getCycleRoot() const; + + static void onTopLevelEvaluationFinished(ModuleObject* module); + + static bool appendAsyncParentModule(JSContext* cx, Handle<ModuleObject*> self, + Handle<ModuleObject*> parent); + + [[nodiscard]] static bool topLevelCapabilityResolve( + JSContext* cx, Handle<ModuleObject*> module); + [[nodiscard]] static bool topLevelCapabilityReject( + JSContext* cx, Handle<ModuleObject*> module, HandleValue error); + + void setMetaObject(JSObject* obj); + + static bool instantiateFunctionDeclarations(JSContext* cx, + Handle<ModuleObject*> self); + + static bool execute(JSContext* cx, Handle<ModuleObject*> self); + + static ModuleNamespaceObject* createNamespace( + JSContext* cx, Handle<ModuleObject*> self, + MutableHandle<UniquePtr<ExportNameVector>> exports); + + static bool createEnvironment(JSContext* cx, Handle<ModuleObject*> self); + + void initAsyncSlots(JSContext* cx, bool hasTopLevelAwait, + Handle<ListObject*> asyncParentModules); + + private: + static const JSClassOps classOps_; + + static void trace(JSTracer* trc, JSObject* obj); + static void finalize(JS::GCContext* gcx, JSObject* obj); + + bool hasCyclicModuleFields() const; + CyclicModuleFields* cyclicModuleFields(); + const CyclicModuleFields* cyclicModuleFields() const; +}; + +JSObject* GetOrCreateModuleMetaObject(JSContext* cx, HandleObject module); + +ModuleObject* CallModuleResolveHook(JSContext* cx, + HandleValue referencingPrivate, + HandleObject moduleRequest); + +JSObject* StartDynamicModuleImport(JSContext* cx, HandleScript script, + HandleValue specifier, HandleValue options); + +bool OnModuleEvaluationFailure(JSContext* cx, HandleObject evaluationPromise, + JS::ModuleErrorBehaviour errorBehaviour); + +bool FinishDynamicModuleImport(JSContext* cx, HandleObject evaluationPromise, + HandleValue referencingPrivate, + HandleObject moduleRequest, + HandleObject promise); + +} // namespace js + +template <> +inline bool JSObject::is<js::ModuleNamespaceObject>() const { + return js::IsDerivedProxyObject(this, + &js::ModuleNamespaceObject::proxyHandler); +} + +#endif /* builtin_ModuleObject_h */ diff --git a/js/src/builtin/Number.js b/js/src/builtin/Number.js new file mode 100644 index 0000000000..2291033286 --- /dev/null +++ b/js/src/builtin/Number.js @@ -0,0 +1,105 @@ +/* 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/. */ + +#if JS_HAS_INTL_API +var numberFormatCache = new_Record(); + +/** + * Format this Number object into a string, using the locale and formatting options + * provided. + * + * Spec: ECMAScript Language Specification, 5.1 edition, 15.7.4.3. + * Spec: ECMAScript Internationalization API Specification, 13.2.1. + */ +function Number_toLocaleString() { + // Steps 1-2. + var x = callFunction(ThisNumberValueForToLocaleString, this); + + // Steps 2-3. + var locales = ArgumentsLength() ? GetArgument(0) : undefined; + var options = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 4. + var numberFormat; + if (locales === undefined && options === undefined) { + // This cache only optimizes for the old ES5 toLocaleString without + // locales and options. + if (!intl_IsRuntimeDefaultLocale(numberFormatCache.runtimeDefaultLocale)) { + numberFormatCache.numberFormat = intl_NumberFormat(locales, options); + numberFormatCache.runtimeDefaultLocale = intl_RuntimeDefaultLocale(); + } + numberFormat = numberFormatCache.numberFormat; + } else { + numberFormat = intl_NumberFormat(locales, options); + } + + // Step 5. + return intl_FormatNumber(numberFormat, x, /* formatToParts = */ false); +} +#endif // JS_HAS_INTL_API + +// ES6 draft ES6 20.1.2.4 +function Number_isFinite(num) { + if (typeof num !== "number") { + return false; + } + return num - num === 0; +} + +// ES6 draft ES6 20.1.2.2 +function Number_isNaN(num) { + if (typeof num !== "number") { + return false; + } + return num !== num; +} + +// ES2021 draft rev 889f2f30cf554b7ed812c0984626db1c8a4997c7 +// 20.1.2.3 Number.isInteger ( number ) +function Number_isInteger(number) { + // Step 1. (Inlined call to IsIntegralNumber) + + // 7.2.6 IsIntegralNumber, step 1. + if (typeof number !== "number") { + return false; + } + + var integer = std_Math_trunc(number); + + // 7.2.6 IsIntegralNumber, steps 2-4. + // |number - integer| ensures Infinity correctly returns false, because + // |Infinity - Infinity| yields NaN. + return number - integer === 0; +} + +// ES2021 draft rev 889f2f30cf554b7ed812c0984626db1c8a4997c7 +// 20.1.2.5 Number.isSafeInteger ( number ) +function Number_isSafeInteger(number) { + // Step 1. (Inlined call to IsIntegralNumber) + + // 7.2.6 IsIntegralNumber, step 1. + if (typeof number !== "number") { + return false; + } + + var integer = std_Math_trunc(number); + + // 7.2.6 IsIntegralNumber, steps 2-4. + // |number - integer| to handle the Infinity case correctly. + if (number - integer !== 0) { + return false; + } + + // Steps 1.a, 2. + // prettier-ignore + return -((2 ** 53) - 1) <= integer && integer <= (2 ** 53) - 1; +} + +function Global_isNaN(number) { + return Number_isNaN(ToNumber(number)); +} + +function Global_isFinite(number) { + return Number_isFinite(ToNumber(number)); +} diff --git a/js/src/builtin/Object.cpp b/js/src/builtin/Object.cpp new file mode 100644 index 0000000000..ece27e5534 --- /dev/null +++ b/js/src/builtin/Object.cpp @@ -0,0 +1,2308 @@ +/* -*- 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 "builtin/Object.h" +#include "js/Object.h" // JS::GetBuiltinClass + +#include "mozilla/Maybe.h" +#include "mozilla/Range.h" +#include "mozilla/RangedPtr.h" + +#include <algorithm> +#include <string_view> + +#include "jsapi.h" + +#include "builtin/Eval.h" +#include "builtin/SelfHostingDefines.h" +#include "frontend/BytecodeCompiler.h" +#include "jit/InlinableNatives.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/friend/StackLimits.h" // js::AutoCheckRecursionLimit +#include "js/PropertySpec.h" +#include "js/UniquePtr.h" +#include "util/StringBuffer.h" +#include "util/Text.h" +#include "vm/BooleanObject.h" +#include "vm/DateObject.h" +#include "vm/EqualityOperations.h" // js::SameValue +#include "vm/ErrorObject.h" +#include "vm/JSContext.h" +#include "vm/NumberObject.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/RegExpObject.h" +#include "vm/StringObject.h" +#include "vm/ToSource.h" // js::ValueToSource +#include "vm/Watchtower.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#ifdef ENABLE_RECORD_TUPLE +# include "builtin/RecordObject.h" +# include "builtin/TupleObject.h" +#endif + +#include "vm/GeckoProfiler-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" +#include "vm/Shape-inl.h" + +#ifdef FUZZING +# include "builtin/TestingFunctions.h" +#endif + +using namespace js; + +using js::frontend::IsIdentifier; + +using mozilla::Maybe; +using mozilla::Range; +using mozilla::RangedPtr; + +static PlainObject* CreateThis(JSContext* cx, HandleObject newTarget) { + RootedObject proto(cx); + if (!GetPrototypeFromConstructor(cx, newTarget, JSProto_Object, &proto)) { + return nullptr; + } + + gc::AllocKind allocKind = NewObjectGCKind(); + + if (proto) { + return NewPlainObjectWithProtoAndAllocKind(cx, proto, allocKind); + } + return NewPlainObjectWithAllocKind(cx, allocKind); +} + +bool js::obj_construct(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JSObject* obj; + if (args.isConstructing() && + (&args.newTarget().toObject() != &args.callee())) { + RootedObject newTarget(cx, &args.newTarget().toObject()); + obj = CreateThis(cx, newTarget); + } else if (args.length() > 0 && !args[0].isNullOrUndefined()) { + obj = ToObject(cx, args[0]); + } else { + /* Make an object whether this was called with 'new' or not. */ + gc::AllocKind allocKind = NewObjectGCKind(); + obj = NewPlainObjectWithAllocKind(cx, allocKind); + } + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/* ES5 15.2.4.7. */ +bool js::obj_propertyIsEnumerable(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + HandleValue idValue = args.get(0); + + // As an optimization, provide a fast path when rooting is not necessary and + // we can safely retrieve the attributes from the object's shape. + + /* Steps 1-2. */ + jsid id; + if (args.thisv().isObject() && idValue.isPrimitive() && + PrimitiveValueToId<NoGC>(cx, idValue, &id)) { + JSObject* obj = &args.thisv().toObject(); + + /* Step 3. */ + PropertyResult prop; + if (obj->is<NativeObject>() && + NativeLookupOwnProperty<NoGC>(cx, &obj->as<NativeObject>(), id, + &prop)) { + /* Step 4. */ + if (prop.isNotFound()) { + args.rval().setBoolean(false); + return true; + } + + /* Step 5. */ + JS::PropertyAttributes attrs = GetPropertyAttributes(obj, prop); + args.rval().setBoolean(attrs.enumerable()); + return true; + } + } + + /* Step 1. */ + RootedId idRoot(cx); + if (!ToPropertyKey(cx, idValue, &idRoot)) { + return false; + } + + /* Step 2. */ + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + /* Step 3. */ + Rooted<Maybe<PropertyDescriptor>> desc(cx); + if (!GetOwnPropertyDescriptor(cx, obj, idRoot, &desc)) { + return false; + } + + /* Step 4. */ + if (desc.isNothing()) { + args.rval().setBoolean(false); + return true; + } + + /* Step 5. */ + args.rval().setBoolean(desc->enumerable()); + return true; +} + +static bool obj_toSource(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Object.prototype", "toSource"); + CallArgs args = CallArgsFromVp(argc, vp); + + AutoCheckRecursionLimit recursion(cx); + if (!recursion.check(cx)) { + return false; + } + + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + JSString* str = ObjectToSource(cx, obj); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +template <typename CharT> +static bool Consume(RangedPtr<const CharT>& s, RangedPtr<const CharT> e, + std::string_view chars) { + MOZ_ASSERT(s <= e); + size_t len = chars.length(); + if (e - s < len) { + return false; + } + if (!EqualChars(s.get(), chars.data(), len)) { + return false; + } + s += len; + return true; +} + +template <typename CharT> +static bool ConsumeUntil(RangedPtr<const CharT>& s, RangedPtr<const CharT> e, + char16_t ch) { + MOZ_ASSERT(s <= e); + const CharT* result = js_strchr_limit(s.get(), ch, e.get()); + if (!result) { + return false; + } + s += result - s.get(); + MOZ_ASSERT(*s == ch); + return true; +} + +template <typename CharT> +static void ConsumeSpaces(RangedPtr<const CharT>& s, RangedPtr<const CharT> e) { + while (s < e && *s == ' ') { + s++; + } +} + +/* + * Given a function source string, return the offset and length of the part + * between '(function $name' and ')'. + */ +template <typename CharT> +static bool ArgsAndBodySubstring(Range<const CharT> chars, size_t* outOffset, + size_t* outLen) { + const RangedPtr<const CharT> start = chars.begin(); + RangedPtr<const CharT> s = start; + RangedPtr<const CharT> e = chars.end(); + + if (s == e) { + return false; + } + + // Remove enclosing parentheses. + if (*s == '(' && *(e - 1) == ')') { + s++; + e--; + } + + // Support the following cases, with spaces between tokens: + // + // -+---------+-+------------+-+-----+-+- [ - <any> - ] - ( -+- + // | | | | | | | | + // +- async -+ +- function -+ +- * -+ +- <any> - ( ---------+ + // | | + // +- get ------+ + // | | + // +- set ------+ + // + // This accepts some invalid syntax, but we don't care, since it's only + // used by the non-standard toSource, and we're doing a best-effort attempt + // here. + + (void)Consume(s, e, "async"); + ConsumeSpaces(s, e); + (void)(Consume(s, e, "function") || Consume(s, e, "get") || + Consume(s, e, "set")); + ConsumeSpaces(s, e); + (void)Consume(s, e, "*"); + ConsumeSpaces(s, e); + + // Jump over the function's name. + if (Consume(s, e, "[")) { + if (!ConsumeUntil(s, e, ']')) { + return false; + } + s++; // Skip ']'. + ConsumeSpaces(s, e); + if (s >= e || *s != '(') { + return false; + } + } else { + if (!ConsumeUntil(s, e, '(')) { + return false; + } + } + + MOZ_ASSERT(*s == '('); + + *outOffset = s - start; + *outLen = e - s; + MOZ_ASSERT(*outOffset + *outLen <= chars.length()); + return true; +} + +enum class PropertyKind { Getter, Setter, Method, Normal }; + +JSString* js::ObjectToSource(JSContext* cx, HandleObject obj) { + /* If outermost, we need parentheses to be an expression, not a block. */ + bool outermost = cx->cycleDetectorVector().empty(); + + AutoCycleDetector detector(cx, obj); + if (!detector.init()) { + return nullptr; + } + if (detector.foundCycle()) { + return NewStringCopyZ<CanGC>(cx, "{}"); + } + + JSStringBuilder buf(cx); + if (outermost && !buf.append('(')) { + return nullptr; + } + if (!buf.append('{')) { + return nullptr; + } + + RootedIdVector idv(cx); + if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY | JSITER_SYMBOLS, &idv)) { + return nullptr; + } + +#ifdef ENABLE_RECORD_TUPLE + if (IsExtendedPrimitiveWrapper(*obj)) { + if (obj->is<TupleObject>()) { + Rooted<TupleType*> tup(cx, &obj->as<TupleObject>().unbox()); + return TupleToSource(cx, tup); + } + MOZ_ASSERT(obj->is<RecordObject>()); + return RecordToSource(cx, obj->as<RecordObject>().unbox()); + } +#endif + + bool comma = false; + + auto AddProperty = [cx, &comma, &buf](HandleId id, HandleValue val, + PropertyKind kind) -> bool { + /* Convert id to a string. */ + RootedString idstr(cx); + if (id.isSymbol()) { + RootedValue v(cx, SymbolValue(id.toSymbol())); + idstr = ValueToSource(cx, v); + if (!idstr) { + return false; + } + } else { + RootedValue idv(cx, IdToValue(id)); + idstr = ToString<CanGC>(cx, idv); + if (!idstr) { + return false; + } + + /* + * If id is a string that's not an identifier, or if it's a + * negative integer, then it must be quoted. + */ + if (id.isAtom() ? !IsIdentifier(id.toAtom()) : id.toInt() < 0) { + UniqueChars quotedId = QuoteString(cx, idstr, '\''); + if (!quotedId) { + return false; + } + idstr = NewStringCopyZ<CanGC>(cx, quotedId.get()); + if (!idstr) { + return false; + } + } + } + + RootedString valsource(cx, ValueToSource(cx, val)); + if (!valsource) { + return false; + } + + Rooted<JSLinearString*> valstr(cx, valsource->ensureLinear(cx)); + if (!valstr) { + return false; + } + + if (comma && !buf.append(", ")) { + return false; + } + comma = true; + + size_t voffset, vlength; + + // Methods and accessors can return exact syntax of source, that fits + // into property without adding property name or "get"/"set" prefix. + // Use the exact syntax when the following conditions are met: + // + // * It's a function object + // (exclude proxies) + // * Function's kind and property's kind are same + // (this can be false for dynamically defined properties) + // * Function has explicit name + // (this can be false for computed property and dynamically defined + // properties) + // * Function's name and property's name are same + // (this can be false for dynamically defined properties) + if (kind == PropertyKind::Getter || kind == PropertyKind::Setter || + kind == PropertyKind::Method) { + RootedFunction fun(cx); + if (val.toObject().is<JSFunction>()) { + fun = &val.toObject().as<JSFunction>(); + // Method's case should be checked on caller. + if (((fun->isGetter() && kind == PropertyKind::Getter) || + (fun->isSetter() && kind == PropertyKind::Setter) || + kind == PropertyKind::Method) && + fun->explicitName()) { + bool result; + if (!EqualStrings(cx, fun->explicitName(), idstr, &result)) { + return false; + } + + if (result) { + if (!buf.append(valstr)) { + return false; + } + return true; + } + } + } + + { + // When falling back try to generate a better string + // representation by skipping the prelude, and also removing + // the enclosing parentheses. + bool success; + JS::AutoCheckCannotGC nogc; + if (valstr->hasLatin1Chars()) { + success = ArgsAndBodySubstring(valstr->latin1Range(nogc), &voffset, + &vlength); + } else { + success = ArgsAndBodySubstring(valstr->twoByteRange(nogc), &voffset, + &vlength); + } + if (!success) { + kind = PropertyKind::Normal; + } + } + + if (kind == PropertyKind::Getter) { + if (!buf.append("get ")) { + return false; + } + } else if (kind == PropertyKind::Setter) { + if (!buf.append("set ")) { + return false; + } + } else if (kind == PropertyKind::Method && fun) { + if (fun->isAsync()) { + if (!buf.append("async ")) { + return false; + } + } + + if (fun->isGenerator()) { + if (!buf.append('*')) { + return false; + } + } + } + } + + bool needsBracket = id.isSymbol(); + if (needsBracket && !buf.append('[')) { + return false; + } + if (!buf.append(idstr)) { + return false; + } + if (needsBracket && !buf.append(']')) { + return false; + } + + if (kind == PropertyKind::Getter || kind == PropertyKind::Setter || + kind == PropertyKind::Method) { + if (!buf.appendSubstring(valstr, voffset, vlength)) { + return false; + } + } else { + if (!buf.append(':')) { + return false; + } + if (!buf.append(valstr)) { + return false; + } + } + return true; + }; + + RootedId id(cx); + Rooted<Maybe<PropertyDescriptor>> desc(cx); + RootedValue val(cx); + for (size_t i = 0; i < idv.length(); ++i) { + id = idv[i]; + if (!GetOwnPropertyDescriptor(cx, obj, id, &desc)) { + return nullptr; + } + + if (desc.isNothing()) { + continue; + } + + if (desc->isAccessorDescriptor()) { + if (desc->hasGetter() && desc->getter()) { + val.setObject(*desc->getter()); + if (!AddProperty(id, val, PropertyKind::Getter)) { + return nullptr; + } + } + if (desc->hasSetter() && desc->setter()) { + val.setObject(*desc->setter()); + if (!AddProperty(id, val, PropertyKind::Setter)) { + return nullptr; + } + } + continue; + } + + val.set(desc->value()); + + JSFunction* fun = nullptr; + if (IsFunctionObject(val, &fun) && fun->isMethod()) { + if (!AddProperty(id, val, PropertyKind::Method)) { + return nullptr; + } + continue; + } + + if (!AddProperty(id, val, PropertyKind::Normal)) { + return nullptr; + } + } + + if (!buf.append('}')) { + return nullptr; + } + if (outermost && !buf.append(')')) { + return nullptr; + } + + return buf.finishString(); +} + +static JSString* GetBuiltinTagSlow(JSContext* cx, HandleObject obj) { + // Step 4. + bool isArray; + if (!IsArray(cx, obj, &isArray)) { + return nullptr; + } + + // Step 5. + if (isArray) { + return cx->names().objectArray; + } + + // Steps 6-14. + ESClass cls; + if (!JS::GetBuiltinClass(cx, obj, &cls)) { + return nullptr; + } + + switch (cls) { + case ESClass::String: + return cx->names().objectString; + case ESClass::Arguments: + return cx->names().objectArguments; + case ESClass::Error: + return cx->names().objectError; + case ESClass::Boolean: + return cx->names().objectBoolean; + case ESClass::Number: + return cx->names().objectNumber; + case ESClass::Date: + return cx->names().objectDate; + case ESClass::RegExp: + return cx->names().objectRegExp; + default: + if (obj->isCallable()) { + // Non-standard: Prevent <object> from showing up as Function. + JSObject* unwrapped = CheckedUnwrapDynamic(obj, cx); + if (!unwrapped || !unwrapped->getClass()->isDOMClass()) { + return cx->names().objectFunction; + } + } + return cx->names().objectObject; + } +} + +static MOZ_ALWAYS_INLINE JSString* GetBuiltinTagFast(JSObject* obj, + JSContext* cx) { + const JSClass* clasp = obj->getClass(); + MOZ_ASSERT(!clasp->isProxyObject()); + + // Optimize the non-proxy case to bypass GetBuiltinClass. + if (clasp == &PlainObject::class_) { + // This case is by far the most common so we handle it first. + return cx->names().objectObject; + } + + if (clasp == &ArrayObject::class_) { + return cx->names().objectArray; + } + + if (clasp->isJSFunction()) { + return cx->names().objectFunction; + } + + if (clasp == &StringObject::class_) { + return cx->names().objectString; + } + + if (clasp == &NumberObject::class_) { + return cx->names().objectNumber; + } + + if (clasp == &BooleanObject::class_) { + return cx->names().objectBoolean; + } + + if (clasp == &DateObject::class_) { + return cx->names().objectDate; + } + + if (clasp == &RegExpObject::class_) { + return cx->names().objectRegExp; + } + + if (obj->is<ArgumentsObject>()) { + return cx->names().objectArguments; + } + + if (obj->is<ErrorObject>()) { + return cx->names().objectError; + } + + if (obj->isCallable() && !obj->getClass()->isDOMClass()) { + // Non-standard: Prevent <object> from showing up as Function. + return cx->names().objectFunction; + } + + return cx->names().objectObject; +} + +// For primitive values we try to avoid allocating the object if we can +// determine that the prototype it would use does not define Symbol.toStringTag. +static JSAtom* MaybeObjectToStringPrimitive(JSContext* cx, const Value& v) { + JSProtoKey protoKey = js::PrimitiveToProtoKey(cx, v); + + // If prototype doesn't exist yet, just fall through. + JSObject* proto = cx->global()->maybeGetPrototype(protoKey); + if (!proto) { + return nullptr; + } + + // If determining this may have side-effects, we must instead create the + // object normally since it is the receiver while looking up + // Symbol.toStringTag. + if (MaybeHasInterestingSymbolProperty( + cx, proto, cx->wellKnownSymbols().toStringTag, nullptr)) { + return nullptr; + } + + // Return the direct result. + switch (protoKey) { + case JSProto_String: + return cx->names().objectString; + case JSProto_Number: + return cx->names().objectNumber; + case JSProto_Boolean: + return cx->names().objectBoolean; + case JSProto_Symbol: + return cx->names().objectSymbol; + case JSProto_BigInt: + return cx->names().objectBigInt; + default: + break; + } + + return nullptr; +} + +// ES6 19.1.3.6 +bool js::obj_toString(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Object.prototype", "toString"); + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject obj(cx); + + if (args.thisv().isPrimitive()) { + // Step 1. + if (args.thisv().isUndefined()) { + args.rval().setString(cx->names().objectUndefined); + return true; + } + + // Step 2. + if (args.thisv().isNull()) { + args.rval().setString(cx->names().objectNull); + return true; + } + + // Try fast-path for primitives. This is unusual but we encounter code like + // this in the wild. + JSAtom* result = MaybeObjectToStringPrimitive(cx, args.thisv()); + if (result) { + args.rval().setString(result); + return true; + } + + // Step 3. + obj = ToObject(cx, args.thisv()); + if (!obj) { + return false; + } + } else { + obj = &args.thisv().toObject(); + } + + // When |obj| is a non-proxy object, compute |builtinTag| only when needed. + RootedString builtinTag(cx); + if (MOZ_UNLIKELY(obj->is<ProxyObject>())) { + builtinTag = GetBuiltinTagSlow(cx, obj); + if (!builtinTag) { + return false; + } + } + + // Step 15. + RootedValue tag(cx); + if (!GetInterestingSymbolProperty(cx, obj, cx->wellKnownSymbols().toStringTag, + &tag)) { + return false; + } + + // Step 16. + if (!tag.isString()) { + if (!builtinTag) { + builtinTag = GetBuiltinTagFast(obj, cx); +#ifdef DEBUG + // Assert this fast path is correct and matches BuiltinTagSlow. + JSString* builtinTagSlow = GetBuiltinTagSlow(cx, obj); + if (!builtinTagSlow) { + return false; + } + MOZ_ASSERT(builtinTagSlow == builtinTag); +#endif + } + + args.rval().setString(builtinTag); + return true; + } + + // Step 17. + StringBuffer sb(cx); + if (!sb.append("[object ") || !sb.append(tag.toString()) || !sb.append(']')) { + return false; + } + + JSString* str = sb.finishAtom(); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +JSString* js::ObjectClassToString(JSContext* cx, JSObject* obj) { + AutoUnsafeCallWithABI unsafe; + + if (MaybeHasInterestingSymbolProperty(cx, obj, + cx->wellKnownSymbols().toStringTag)) { + return nullptr; + } + return GetBuiltinTagFast(obj, cx); +} + +static bool obj_setPrototypeOf(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "Object.setPrototypeOf", 2)) { + return false; + } + + /* Step 1-2. */ + if (args[0].isNullOrUndefined()) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_CANT_CONVERT_TO, + args[0].isNull() ? "null" : "undefined", "object"); + return false; + } + + /* Step 3. */ + if (!args[1].isObjectOrNull()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, "Object.setPrototypeOf", + "an object or null", + InformalValueTypeName(args[1])); + return false; + } + + /* Step 4. */ + if (!args[0].isObject()) { + args.rval().set(args[0]); + return true; + } + + /* Step 5-7. */ + RootedObject obj(cx, &args[0].toObject()); + RootedObject newProto(cx, args[1].toObjectOrNull()); + if (!SetPrototype(cx, obj, newProto)) { + return false; + } + + /* Step 8. */ + args.rval().set(args[0]); + return true; +} + +static bool PropertyIsEnumerable(JSContext* cx, HandleObject obj, HandleId id, + bool* enumerable) { + PropertyResult prop; + if (obj->is<NativeObject>() && + NativeLookupOwnProperty<NoGC>(cx, &obj->as<NativeObject>(), id, &prop)) { + if (prop.isNotFound()) { + *enumerable = false; + return true; + } + + JS::PropertyAttributes attrs = GetPropertyAttributes(obj, prop); + *enumerable = attrs.enumerable(); + return true; + } + + Rooted<Maybe<PropertyDescriptor>> desc(cx); + if (!GetOwnPropertyDescriptor(cx, obj, id, &desc)) { + return false; + } + + *enumerable = desc.isSome() && desc->enumerable(); + return true; +} + +// Returns true if properties not named "__proto__" can be added to |obj| +// with a fast path that doesn't check any properties on the prototype chain. +static bool CanAddNewPropertyExcludingProtoFast(PlainObject* obj) { + if (!obj->isExtensible() || obj->isUsedAsPrototype()) { + return false; + } + + // Ensure the object has no non-writable properties or getters/setters. + // For now only support PlainObjects so that we don't have to worry about + // resolve hooks and other JSClass hooks. + while (true) { + if (obj->hasNonWritableOrAccessorPropExclProto()) { + return false; + } + + JSObject* proto = obj->staticPrototype(); + if (!proto) { + return true; + } + if (!proto->is<PlainObject>()) { + return false; + } + obj = &proto->as<PlainObject>(); + } +} + +[[nodiscard]] static bool TryAssignPlain(JSContext* cx, HandleObject to, + HandleObject from, bool* optimized) { + // Object.assign is used with PlainObjects most of the time. This is a fast + // path to optimize that case. This lets us avoid checks that are only + // relevant for other JSClasses. + + MOZ_ASSERT(*optimized == false); + + if (!from->is<PlainObject>() || !to->is<PlainObject>()) { + return true; + } + + // Don't use the fast path if |from| may have extra indexed properties. + Handle<PlainObject*> fromPlain = from.as<PlainObject>(); + if (fromPlain->getDenseInitializedLength() > 0 || fromPlain->isIndexed()) { + return true; + } + MOZ_ASSERT(!fromPlain->getClass()->getNewEnumerate()); + MOZ_ASSERT(!fromPlain->getClass()->getEnumerate()); + + // Empty |from| objects are common, so check for this first. + if (fromPlain->empty()) { + *optimized = true; + return true; + } + + Handle<PlainObject*> toPlain = to.as<PlainObject>(); + if (!CanAddNewPropertyExcludingProtoFast(toPlain)) { + return true; + } + + // Get a list of all enumerable |from| properties. + + Rooted<PropertyInfoWithKeyVector> props(cx, PropertyInfoWithKeyVector(cx)); + +#ifdef DEBUG + Rooted<Shape*> fromShape(cx, fromPlain->shape()); +#endif + + bool hasPropsWithNonDefaultAttrs = false; + for (ShapePropertyIter<NoGC> iter(fromPlain->shape()); !iter.done(); iter++) { + // Symbol properties need to be assigned last. For now fall back to the + // slow path if we see a symbol property. + jsid id = iter->key(); + if (MOZ_UNLIKELY(id.isSymbol())) { + return true; + } + // __proto__ is not supported by CanAddNewPropertyExcludingProtoFast. + if (MOZ_UNLIKELY(id.isAtom(cx->names().proto))) { + return true; + } + if (MOZ_UNLIKELY(!iter->isDataProperty())) { + return true; + } + if (iter->flags() != PropertyFlags::defaultDataPropFlags) { + hasPropsWithNonDefaultAttrs = true; + } + if (!iter->enumerable()) { + continue; + } + if (MOZ_UNLIKELY(!props.append(*iter))) { + return false; + } + } + + *optimized = true; + + bool toWasEmpty = toPlain->empty(); + + // If the |to| object has no properties and the |from| object only has plain + // enumerable/writable/configurable data properties, try to use its shape. + if (toWasEmpty && !hasPropsWithNonDefaultAttrs && + toPlain->canReuseShapeForNewProperties(fromPlain->shape())) { + MOZ_ASSERT(!Watchtower::watchesPropertyAdd(toPlain), + "watched objects require Watchtower calls"); + SharedShape* newShape = fromPlain->sharedShape(); + uint32_t oldSpan = 0; + uint32_t newSpan = props.length(); + if (!toPlain->setShapeAndAddNewSlots(cx, newShape, oldSpan, newSpan)) { + return false; + } + for (size_t i = props.length(); i > 0; i--) { + size_t slot = props[i - 1].slot(); + toPlain->initSlot(slot, fromPlain->getSlot(slot)); + } + return true; + } + + RootedValue propValue(cx); + RootedId nextKey(cx); + + for (size_t i = props.length(); i > 0; i--) { + // Assert |from| still has the same properties. + MOZ_ASSERT(fromPlain->shape() == fromShape); + + PropertyInfoWithKey fromProp = props[i - 1]; + MOZ_ASSERT(fromProp.isDataProperty()); + MOZ_ASSERT(fromProp.enumerable()); + + nextKey = fromProp.key(); + propValue = fromPlain->getSlot(fromProp.slot()); + + Maybe<PropertyInfo> toProp; + if (toWasEmpty) { + MOZ_ASSERT(!toPlain->containsPure(nextKey)); + MOZ_ASSERT(toProp.isNothing()); + } else { + toProp = toPlain->lookup(cx, nextKey); + } + + if (toProp.isSome()) { + MOZ_ASSERT(toProp->isDataProperty()); + MOZ_ASSERT(toProp->writable()); + toPlain->setSlot(toProp->slot(), propValue); + } else { + if (!AddDataPropertyToPlainObject(cx, toPlain, nextKey, propValue)) { + return false; + } + } + } + + return true; +} + +static bool TryAssignNative(JSContext* cx, HandleObject to, HandleObject from, + bool* optimized) { + MOZ_ASSERT(*optimized == false); + + if (!from->is<NativeObject>() || !to->is<NativeObject>()) { + return true; + } + + // Don't use the fast path if |from| may have extra indexed or lazy + // properties. + NativeObject* fromNative = &from->as<NativeObject>(); + if (fromNative->getDenseInitializedLength() > 0 || fromNative->isIndexed() || + fromNative->is<TypedArrayObject>() || + fromNative->getClass()->getNewEnumerate() || + fromNative->getClass()->getEnumerate()) { + return true; + } + + // Get a list of |from| properties. As long as from->shape() == fromShape + // we can use this to speed up both the enumerability check and the GetProp. + + Rooted<PropertyInfoWithKeyVector> props(cx, PropertyInfoWithKeyVector(cx)); + + Rooted<NativeShape*> fromShape(cx, fromNative->shape()); + for (ShapePropertyIter<NoGC> iter(fromShape); !iter.done(); iter++) { + // Symbol properties need to be assigned last. For now fall back to the + // slow path if we see a symbol property. + if (MOZ_UNLIKELY(iter->key().isSymbol())) { + return true; + } + if (MOZ_UNLIKELY(!props.append(*iter))) { + return false; + } + } + + *optimized = true; + + RootedValue propValue(cx); + RootedId nextKey(cx); + RootedValue toReceiver(cx, ObjectValue(*to)); + + for (size_t i = props.length(); i > 0; i--) { + PropertyInfoWithKey prop = props[i - 1]; + nextKey = prop.key(); + + // If |from| still has the same shape, it must still be a NativeObject with + // the properties in |props|. + if (MOZ_LIKELY(from->shape() == fromShape && prop.isDataProperty())) { + if (!prop.enumerable()) { + continue; + } + propValue = from->as<NativeObject>().getSlot(prop.slot()); + } else { + // |from| changed shape or the property is not a data property, so + // we have to do the slower enumerability check and GetProp. + bool enumerable; + if (!PropertyIsEnumerable(cx, from, nextKey, &enumerable)) { + return false; + } + if (!enumerable) { + continue; + } + if (!GetProperty(cx, from, from, nextKey, &propValue)) { + return false; + } + } + + ObjectOpResult result; + if (MOZ_UNLIKELY( + !SetProperty(cx, to, nextKey, propValue, toReceiver, result))) { + return false; + } + if (MOZ_UNLIKELY(!result.checkStrict(cx, to, nextKey))) { + return false; + } + } + + return true; +} + +static bool AssignSlow(JSContext* cx, HandleObject to, HandleObject from) { + // Step 4.b.ii. + RootedIdVector keys(cx); + if (!GetPropertyKeys( + cx, from, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS, &keys)) { + return false; + } + + // Step 4.c. + RootedId nextKey(cx); + RootedValue propValue(cx); + for (size_t i = 0, len = keys.length(); i < len; i++) { + nextKey = keys[i]; + + // Step 4.c.i. + bool enumerable; + if (MOZ_UNLIKELY(!PropertyIsEnumerable(cx, from, nextKey, &enumerable))) { + return false; + } + if (!enumerable) { + continue; + } + + // Step 4.c.ii.1. + if (MOZ_UNLIKELY(!GetProperty(cx, from, from, nextKey, &propValue))) { + return false; + } + + // Step 4.c.ii.2. + if (MOZ_UNLIKELY(!SetProperty(cx, to, nextKey, propValue))) { + return false; + } + } + + return true; +} + +JS_PUBLIC_API bool JS_AssignObject(JSContext* cx, JS::HandleObject target, + JS::HandleObject src) { + bool optimized = false; + + if (!TryAssignPlain(cx, target, src, &optimized)) { + return false; + } + if (optimized) { + return true; + } + + if (!TryAssignNative(cx, target, src, &optimized)) { + return false; + } + if (optimized) { + return true; + } + + return AssignSlow(cx, target, src); +} + +// ES2018 draft rev 48ad2688d8f964da3ea8c11163ef20eb126fb8a4 +// 19.1.2.1 Object.assign(target, ...sources) +static bool obj_assign(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Object", "assign"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject to(cx, ToObject(cx, args.get(0))); + if (!to) { + return false; + } + + // Note: step 2 is implicit. If there are 0 arguments, ToObject throws. If + // there's 1 argument, the loop below is a no-op. + + // Step 4. + RootedObject from(cx); + for (size_t i = 1; i < args.length(); i++) { + // Step 4.a. + if (args[i].isNullOrUndefined()) { + continue; + } + + // Step 4.b.i. + from = ToObject(cx, args[i]); + if (!from) { + return false; + } + + // Steps 4.b.ii, 4.c. + if (!JS_AssignObject(cx, to, from)) { + return false; + } + } + + // Step 5. + args.rval().setObject(*to); + return true; +} + +/* ES5 15.2.4.6. */ +bool js::obj_isPrototypeOf(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + /* Step 1. */ + if (args.length() < 1 || !args[0].isObject()) { + args.rval().setBoolean(false); + return true; + } + + /* Step 2. */ + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) { + return false; + } + + /* Step 3. */ + bool isPrototype; + if (!IsPrototypeOf(cx, obj, &args[0].toObject(), &isPrototype)) { + return false; + } + args.rval().setBoolean(isPrototype); + return true; +} + +PlainObject* js::ObjectCreateImpl(JSContext* cx, HandleObject proto, + NewObjectKind newKind) { + // Give the new object a small number of fixed slots, like we do for empty + // object literals ({}). + gc::AllocKind allocKind = NewObjectGCKind(); + return NewPlainObjectWithProtoAndAllocKind(cx, proto, allocKind, newKind); +} + +PlainObject* js::ObjectCreateWithTemplate(JSContext* cx, + Handle<PlainObject*> templateObj) { + RootedObject proto(cx, templateObj->staticPrototype()); + return ObjectCreateImpl(cx, proto, GenericObject); +} + +// ES 2017 draft 19.1.2.3.1 +static bool ObjectDefineProperties(JSContext* cx, HandleObject obj, + HandleValue properties, + bool* failedOnWindowProxy) { + // Step 1. implicit + // Step 2. + RootedObject props(cx, ToObject(cx, properties)); + if (!props) { + return false; + } + + // Step 3. + RootedIdVector keys(cx); + if (!GetPropertyKeys( + cx, props, JSITER_OWNONLY | JSITER_SYMBOLS | JSITER_HIDDEN, &keys)) { + return false; + } + + RootedId nextKey(cx); + Rooted<Maybe<PropertyDescriptor>> keyDesc(cx); + Rooted<PropertyDescriptor> desc(cx); + RootedValue descObj(cx); + + // Step 4. + Rooted<PropertyDescriptorVector> descriptors(cx, + PropertyDescriptorVector(cx)); + RootedIdVector descriptorKeys(cx); + + // Step 5. + for (size_t i = 0, len = keys.length(); i < len; i++) { + nextKey = keys[i]; + + // Step 5.a. + if (!GetOwnPropertyDescriptor(cx, props, nextKey, &keyDesc)) { + return false; + } + + // Step 5.b. + if (keyDesc.isSome() && keyDesc->enumerable()) { + if (!GetProperty(cx, props, props, nextKey, &descObj) || + !ToPropertyDescriptor(cx, descObj, true, &desc) || + !descriptors.append(desc) || !descriptorKeys.append(nextKey)) { + return false; + } + } + } + + // Step 6. + *failedOnWindowProxy = false; + for (size_t i = 0, len = descriptors.length(); i < len; i++) { + ObjectOpResult result; + if (!DefineProperty(cx, obj, descriptorKeys[i], descriptors[i], result)) { + return false; + } + + if (!result.ok()) { + if (result.failureCode() == JSMSG_CANT_DEFINE_WINDOW_NC) { + *failedOnWindowProxy = true; + } else if (!result.checkStrict(cx, obj, descriptorKeys[i])) { + return false; + } + } + } + + return true; +} + +// ES6 draft rev34 (2015/02/20) 19.1.2.2 Object.create(O [, Properties]) +bool js::obj_create(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!args.requireAtLeast(cx, "Object.create", 1)) { + return false; + } + + if (!args[0].isObjectOrNull()) { + UniqueChars bytes = + DecompileValueGenerator(cx, JSDVG_SEARCH_STACK, args[0], nullptr); + if (!bytes) { + return false; + } + + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_UNEXPECTED_TYPE, bytes.get(), + "not an object or null"); + return false; + } + + // Step 2. + RootedObject proto(cx, args[0].toObjectOrNull()); + Rooted<PlainObject*> obj(cx, ObjectCreateImpl(cx, proto)); + if (!obj) { + return false; + } + + // Step 3. + if (args.hasDefined(1)) { + // we can't ever end up with failures to define on a WindowProxy + // here, because "obj" is never a WindowProxy. + bool failedOnWindowProxy = false; + if (!ObjectDefineProperties(cx, obj, args[1], &failedOnWindowProxy)) { + return false; + } + MOZ_ASSERT(!failedOnWindowProxy, "How did we get a WindowProxy here?"); + } + + // Step 4. + args.rval().setObject(*obj); + return true; +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 6.2.4.4 FromPropertyDescriptor ( Desc ) +static bool FromPropertyDescriptorToArray( + JSContext* cx, Handle<Maybe<PropertyDescriptor>> desc, + MutableHandleValue vp) { + // Step 1. + if (desc.isNothing()) { + vp.setUndefined(); + return true; + } + + // Steps 2-11. + // Retrieve all property descriptor fields and place them into the result + // array. The actual return object is created in self-hosted code for + // performance reasons. + + int32_t attrsAndKind = 0; + if (desc->enumerable()) { + attrsAndKind |= ATTR_ENUMERABLE; + } + if (desc->configurable()) { + attrsAndKind |= ATTR_CONFIGURABLE; + } + if (!desc->isAccessorDescriptor()) { + if (desc->writable()) { + attrsAndKind |= ATTR_WRITABLE; + } + attrsAndKind |= DATA_DESCRIPTOR_KIND; + } else { + attrsAndKind |= ACCESSOR_DESCRIPTOR_KIND; + } + + Rooted<ArrayObject*> result(cx); + if (!desc->isAccessorDescriptor()) { + result = NewDenseFullyAllocatedArray(cx, 2); + if (!result) { + return false; + } + result->setDenseInitializedLength(2); + + result->initDenseElement(PROP_DESC_ATTRS_AND_KIND_INDEX, + Int32Value(attrsAndKind)); + result->initDenseElement(PROP_DESC_VALUE_INDEX, desc->value()); + } else { + result = NewDenseFullyAllocatedArray(cx, 3); + if (!result) { + return false; + } + result->setDenseInitializedLength(3); + + result->initDenseElement(PROP_DESC_ATTRS_AND_KIND_INDEX, + Int32Value(attrsAndKind)); + + if (JSObject* get = desc->getter()) { + result->initDenseElement(PROP_DESC_GETTER_INDEX, ObjectValue(*get)); + } else { + result->initDenseElement(PROP_DESC_GETTER_INDEX, UndefinedValue()); + } + + if (JSObject* set = desc->setter()) { + result->initDenseElement(PROP_DESC_SETTER_INDEX, ObjectValue(*set)); + } else { + result->initDenseElement(PROP_DESC_SETTER_INDEX, UndefinedValue()); + } + } + + vp.setObject(*result); + return true; +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 19.1.2.6 Object.getOwnPropertyDescriptor ( O, P ) +bool js::GetOwnPropertyDescriptorToArray(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + + // Step 1. + RootedObject obj(cx, ToObject(cx, args[0])); + if (!obj) { + return false; + } + + // Step 2. + RootedId id(cx); + if (!ToPropertyKey(cx, args[1], &id)) { + return false; + } + + // Step 3. + Rooted<Maybe<PropertyDescriptor>> desc(cx); + if (!GetOwnPropertyDescriptor(cx, obj, id, &desc)) { + return false; + } + + // Step 4. + return FromPropertyDescriptorToArray(cx, desc, args.rval()); +} + +static bool NewValuePair(JSContext* cx, HandleValue val1, HandleValue val2, + MutableHandleValue rval) { + ArrayObject* array = NewDenseFullyAllocatedArray(cx, 2); + if (!array) { + return false; + } + + array->setDenseInitializedLength(2); + array->initDenseElement(0, val1); + array->initDenseElement(1, val2); + + rval.setObject(*array); + return true; +} + +enum class EnumerableOwnPropertiesKind { Keys, Values, KeysAndValues, Names }; + +static bool HasEnumerableStringNonDataProperties(NativeObject* obj) { + // We also check for enumerability and symbol properties, so uninteresting + // non-data properties like |array.length| don't let us fall into the slow + // path. + if (!obj->hasEnumerableProperty()) { + return false; + } + for (ShapePropertyIter<NoGC> iter(obj->shape()); !iter.done(); iter++) { + if (!iter->isDataProperty() && iter->enumerable() && + !iter->key().isSymbol()) { + return true; + } + } + return false; +} + +template <EnumerableOwnPropertiesKind kind> +static bool TryEnumerableOwnPropertiesNative(JSContext* cx, HandleObject obj, + MutableHandleValue rval, + bool* optimized) { + *optimized = false; + + // Use the fast path if |obj| has neither extra indexed properties nor a + // newEnumerate hook. String objects need to be special-cased, because + // they're only marked as indexed after their enumerate hook ran. And + // because their enumerate hook is slowish, it's more performant to + // exclude them directly instead of executing the hook first. + if (!obj->is<NativeObject>() || obj->as<NativeObject>().isIndexed() || + obj->getClass()->getNewEnumerate() || obj->is<StringObject>()) { + return true; + } + +#ifdef ENABLE_RECORD_TUPLE + if (obj->is<TupleObject>()) { + Rooted<TupleType*> tup(cx, &obj->as<TupleObject>().unbox()); + return TryEnumerableOwnPropertiesNative<kind>(cx, tup, rval, optimized); + } else if (obj->is<RecordObject>()) { + Rooted<RecordType*> tup(cx, obj->as<RecordObject>().unbox()); + return TryEnumerableOwnPropertiesNative<kind>(cx, tup, rval, optimized); + } +#endif + + Handle<NativeObject*> nobj = obj.as<NativeObject>(); + + // Resolve lazy properties on |nobj|. + if (JSEnumerateOp enumerate = nobj->getClass()->getEnumerate()) { + if (!enumerate(cx, nobj)) { + return false; + } + + // Ensure no extra indexed properties were added through enumerate(). + if (nobj->isIndexed()) { + return true; + } + } + + *optimized = true; + + RootedValueVector properties(cx); + RootedValue key(cx); + RootedValue value(cx); + + // We have ensured |nobj| contains no extra indexed properties, so the + // only indexed properties we need to handle here are dense and typed + // array elements. + + for (uint32_t i = 0, len = nobj->getDenseInitializedLength(); i < len; i++) { + value.set(nobj->getDenseElement(i)); + if (value.isMagic(JS_ELEMENTS_HOLE)) { + continue; + } + + JSString* str; + if (kind != EnumerableOwnPropertiesKind::Values) { + static_assert( + NativeObject::MAX_DENSE_ELEMENTS_COUNT <= PropertyKey::IntMax, + "dense elements don't exceed PropertyKey::IntMax"); + str = Int32ToString<CanGC>(cx, i); + if (!str) { + return false; + } + } + + if (kind == EnumerableOwnPropertiesKind::Keys || + kind == EnumerableOwnPropertiesKind::Names) { + value.setString(str); + } else if (kind == EnumerableOwnPropertiesKind::KeysAndValues) { + key.setString(str); + if (!NewValuePair(cx, key, value, &value)) { + return false; + } + } + + if (!properties.append(value)) { + return false; + } + } + + if (obj->is<TypedArrayObject>()) { + Handle<TypedArrayObject*> tobj = obj.as<TypedArrayObject>(); + size_t len = tobj->length(); + + // Fail early if the typed array contains too many elements for a + // dense array, because we likely OOM anyway when trying to allocate + // more than 2GB for the properties vector. This also means we don't + // need to handle indices greater than MAX_INT32 in the loop below. + if (len > NativeObject::MAX_DENSE_ELEMENTS_COUNT) { + ReportOutOfMemory(cx); + return false; + } + + MOZ_ASSERT(properties.empty(), "typed arrays cannot have dense elements"); + if (!properties.resize(len)) { + return false; + } + + for (uint32_t i = 0; i < len; i++) { + JSString* str; + if (kind != EnumerableOwnPropertiesKind::Values) { + static_assert( + NativeObject::MAX_DENSE_ELEMENTS_COUNT <= PropertyKey::IntMax, + "dense elements don't exceed PropertyKey::IntMax"); + str = Int32ToString<CanGC>(cx, i); + if (!str) { + return false; + } + } + + if (kind == EnumerableOwnPropertiesKind::Keys || + kind == EnumerableOwnPropertiesKind::Names) { + value.setString(str); + } else if (kind == EnumerableOwnPropertiesKind::Values) { + if (!tobj->getElement<CanGC>(cx, i, &value)) { + return false; + } + } else { + key.setString(str); + if (!tobj->getElement<CanGC>(cx, i, &value)) { + return false; + } + if (!NewValuePair(cx, key, value, &value)) { + return false; + } + } + + properties[i].set(value); + } + } +#ifdef ENABLE_RECORD_TUPLE + else if (obj->is<RecordType>()) { + RecordType* rec = &obj->as<RecordType>(); + Rooted<ArrayObject*> keys(cx, rec->keys()); + RootedId keyId(cx); + RootedString keyStr(cx); + + MOZ_ASSERT(properties.empty(), "records cannot have dense elements"); + if (!properties.resize(keys->length())) { + return false; + } + + for (size_t i = 0; i < keys->length(); i++) { + MOZ_ASSERT(keys->getDenseElement(i).isString()); + if (kind == EnumerableOwnPropertiesKind::Keys || + kind == EnumerableOwnPropertiesKind::Names) { + value.set(keys->getDenseElement(i)); + } else if (kind == EnumerableOwnPropertiesKind::Values) { + keyStr.set(keys->getDenseElement(i).toString()); + + if (!JS_StringToId(cx, keyStr, &keyId)) { + return false; + } + MOZ_ALWAYS_TRUE(rec->getOwnProperty(cx, keyId, &value)); + } else { + MOZ_ASSERT(kind == EnumerableOwnPropertiesKind::KeysAndValues); + + key.set(keys->getDenseElement(i)); + keyStr.set(key.toString()); + + if (!JS_StringToId(cx, keyStr, &keyId)) { + return false; + } + MOZ_ALWAYS_TRUE(rec->getOwnProperty(cx, keyId, &value)); + + if (!NewValuePair(cx, key, value, &value)) { + return false; + } + } + + properties[i].set(value); + } + + // Uh, goto... When using records, we already get the (sorted) properties + // from its sorted keys, so we don't read them again as "own properties". + // We could use an `if` or some refactoring to skip the next logic, but + // goto makes it easer to keep the logic separated in + // "#ifdef ENABLE_RECORD_TUPLE" blocks. + // This should be refactored when the #ifdefs are removed. + goto end; + } +#endif + + // Up to this point no side-effects through accessor properties are + // possible which could have replaced |obj| with a non-native object. + MOZ_ASSERT(obj->is<NativeObject>()); + + if (kind == EnumerableOwnPropertiesKind::Keys || + kind == EnumerableOwnPropertiesKind::Names || + !HasEnumerableStringNonDataProperties(nobj)) { + // If |kind == Values| or |kind == KeysAndValues|: + // All enumerable properties with string property keys are data + // properties. This allows us to collect the property values while + // iterating over the shape hierarchy without worrying over accessors + // modifying any state. + + constexpr bool onlyEnumerable = kind != EnumerableOwnPropertiesKind::Names; + if (!onlyEnumerable || nobj->hasEnumerableProperty()) { + size_t elements = properties.length(); + constexpr AllowGC allowGC = + kind != EnumerableOwnPropertiesKind::KeysAndValues ? AllowGC::NoGC + : AllowGC::CanGC; + mozilla::Maybe<ShapePropertyIter<allowGC>> m; + if constexpr (allowGC == AllowGC::NoGC) { + m.emplace(nobj->shape()); + } else { + m.emplace(cx, nobj->shape()); + } + for (auto& iter = m.ref(); !iter.done(); iter++) { + jsid id = iter->key(); + if ((onlyEnumerable && !iter->enumerable()) || id.isSymbol()) { + continue; + } + MOZ_ASSERT(!id.isInt(), "Unexpected indexed property"); + MOZ_ASSERT_IF(kind == EnumerableOwnPropertiesKind::Values || + kind == EnumerableOwnPropertiesKind::KeysAndValues, + iter->isDataProperty()); + + if constexpr (kind == EnumerableOwnPropertiesKind::Keys || + kind == EnumerableOwnPropertiesKind::Names) { + value.setString(id.toString()); + } else if constexpr (kind == EnumerableOwnPropertiesKind::Values) { + value.set(nobj->getSlot(iter->slot())); + } else { + key.setString(id.toString()); + value.set(nobj->getSlot(iter->slot())); + if (!NewValuePair(cx, key, value, &value)) { + return false; + } + } + + if (!properties.append(value)) { + return false; + } + } + + // The (non-indexed) properties were visited in reverse iteration order, + // call std::reverse() to ensure they appear in iteration order. + std::reverse(properties.begin() + elements, properties.end()); + } + } else { + MOZ_ASSERT(kind == EnumerableOwnPropertiesKind::Values || + kind == EnumerableOwnPropertiesKind::KeysAndValues); + + // Get a list of all |obj| properties. As long as obj->shape() + // is equal to |objShape|, we can use this to speed up both the + // enumerability check and GetProperty. + Rooted<PropertyInfoWithKeyVector> props(cx, PropertyInfoWithKeyVector(cx)); + + // Collect all non-symbol properties. + Rooted<NativeShape*> objShape(cx, nobj->shape()); + for (ShapePropertyIter<NoGC> iter(objShape); !iter.done(); iter++) { + if (iter->key().isSymbol()) { + continue; + } + MOZ_ASSERT(!iter->key().isInt(), "Unexpected indexed property"); + + if (!props.append(*iter)) { + return false; + } + } + + RootedId id(cx); + for (size_t i = props.length(); i > 0; i--) { + PropertyInfoWithKey prop = props[i - 1]; + id = prop.key(); + + // If |obj| still has the same shape, it must still be a NativeObject with + // the properties in |props|. + if (obj->shape() == objShape && prop.isDataProperty()) { + if (!prop.enumerable()) { + continue; + } + value = obj->as<NativeObject>().getSlot(prop.slot()); + } else { + // |obj| changed shape or the property is not a data property, + // so we have to do the slower enumerability check and + // GetProperty. + bool enumerable; + if (!PropertyIsEnumerable(cx, obj, id, &enumerable)) { + return false; + } + if (!enumerable) { + continue; + } + if (!GetProperty(cx, obj, obj, id, &value)) { + return false; + } + } + + if (kind == EnumerableOwnPropertiesKind::KeysAndValues) { + key.setString(id.toString()); + if (!NewValuePair(cx, key, value, &value)) { + return false; + } + } + + if (!properties.append(value)) { + return false; + } + } + } + +#ifdef ENABLE_RECORD_TUPLE +end: +#endif + + JSObject* array = + NewDenseCopiedArray(cx, properties.length(), properties.begin()); + if (!array) { + return false; + } + + rval.setObject(*array); + return true; +} + +// ES2018 draft rev c164be80f7ea91de5526b33d54e5c9321ed03d3f +// 7.3.21 EnumerableOwnProperties ( O, kind ) +template <EnumerableOwnPropertiesKind kind> +static bool EnumerableOwnProperties(JSContext* cx, const JS::CallArgs& args) { + static_assert(kind == EnumerableOwnPropertiesKind::Values || + kind == EnumerableOwnPropertiesKind::KeysAndValues, + "Only implemented for Object.keys and Object.entries"); + + // Step 1. (Step 1 of Object.{keys,values,entries}, really.) + RootedObject obj(cx, IF_RECORD_TUPLE(ToObjectOrGetObjectPayload, ToObject)( + cx, args.get(0))); + if (!obj) { + return false; + } + + bool optimized; + if (!TryEnumerableOwnPropertiesNative<kind>(cx, obj, args.rval(), + &optimized)) { + return false; + } + if (optimized) { + return true; + } + + // Typed arrays are always handled in the fast path. + MOZ_ASSERT(!obj->is<TypedArrayObject>()); + + // Step 2. + RootedIdVector ids(cx); + if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY | JSITER_HIDDEN, &ids)) { + return false; + } + + // Step 3. + RootedValueVector properties(cx); + size_t len = ids.length(); + if (!properties.resize(len)) { + return false; + } + + RootedId id(cx); + RootedValue key(cx); + RootedValue value(cx); + Rooted<Shape*> shape(cx); + Rooted<Maybe<PropertyDescriptor>> desc(cx); + // Step 4. + size_t out = 0; + for (size_t i = 0; i < len; i++) { + id = ids[i]; + + // Step 4.a. (Symbols were filtered out in step 2.) + MOZ_ASSERT(!id.isSymbol()); + + if (kind != EnumerableOwnPropertiesKind::Values) { + if (!IdToStringOrSymbol(cx, id, &key)) { + return false; + } + } + + // Step 4.a.i. + if (obj->is<NativeObject>()) { + Handle<NativeObject*> nobj = obj.as<NativeObject>(); + if (id.isInt() && nobj->containsDenseElement(id.toInt())) { + value.set(nobj->getDenseElement(id.toInt())); + } else { + Maybe<PropertyInfo> prop = nobj->lookup(cx, id); + if (prop.isNothing() || !prop->enumerable()) { + continue; + } + if (prop->isDataProperty()) { + value = nobj->getSlot(prop->slot()); + } else if (!GetProperty(cx, obj, obj, id, &value)) { + return false; + } + } + } else { + if (!GetOwnPropertyDescriptor(cx, obj, id, &desc)) { + return false; + } + + // Step 4.a.ii. (inverted.) + if (desc.isNothing() || !desc->enumerable()) { + continue; + } + + // Step 4.a.ii.1. + // (Omitted because Object.keys doesn't use this implementation.) + + // Step 4.a.ii.2.a. + if (!GetProperty(cx, obj, obj, id, &value)) { + return false; + } + } + + // Steps 4.a.ii.2.b-c. + if (kind == EnumerableOwnPropertiesKind::Values) { + properties[out++].set(value); + } else if (!NewValuePair(cx, key, value, properties[out++])) { + return false; + } + } + + // Step 5. + // (Implemented in step 2.) + + // Step 3 of Object.{keys,values,entries} + JSObject* aobj = NewDenseCopiedArray(cx, out, properties.begin()); + if (!aobj) { + return false; + } + + args.rval().setObject(*aobj); + return true; +} + +// ES2018 draft rev c164be80f7ea91de5526b33d54e5c9321ed03d3f +// 19.1.2.16 Object.keys ( O ) +static bool obj_keys(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Object", "keys"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject obj(cx, IF_RECORD_TUPLE(ToObjectOrGetObjectPayload, ToObject)( + cx, args.get(0))); + if (!obj) { + return false; + } + + bool optimized; + static constexpr EnumerableOwnPropertiesKind kind = + EnumerableOwnPropertiesKind::Keys; + if (!TryEnumerableOwnPropertiesNative<kind>(cx, obj, args.rval(), + &optimized)) { + return false; + } + if (optimized) { + return true; + } + + // Steps 2-3. + return GetOwnPropertyKeys(cx, obj, JSITER_OWNONLY, args.rval()); +} + +// ES2018 draft rev c164be80f7ea91de5526b33d54e5c9321ed03d3f +// 19.1.2.21 Object.values ( O ) +static bool obj_values(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Object", "values"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-3. + return EnumerableOwnProperties<EnumerableOwnPropertiesKind::Values>(cx, args); +} + +// ES2018 draft rev c164be80f7ea91de5526b33d54e5c9321ed03d3f +// 19.1.2.5 Object.entries ( O ) +static bool obj_entries(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Object", "entries"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-3. + return EnumerableOwnProperties<EnumerableOwnPropertiesKind::KeysAndValues>( + cx, args); +} + +/* ES6 draft 15.2.3.16 */ +bool js::obj_is(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + bool same; + if (!SameValue(cx, args.get(0), args.get(1), &same)) { + return false; + } + + args.rval().setBoolean(same); + return true; +} + +bool js::IdToStringOrSymbol(JSContext* cx, HandleId id, + MutableHandleValue result) { + if (id.isInt()) { + JSString* str = Int32ToString<CanGC>(cx, id.toInt()); + if (!str) { + return false; + } + result.setString(str); + } else if (id.isAtom()) { + result.setString(id.toAtom()); + } else { + result.setSymbol(id.toSymbol()); + } + return true; +} + +// ES2018 draft rev c164be80f7ea91de5526b33d54e5c9321ed03d3f +// 19.1.2.10.1 Runtime Semantics: GetOwnPropertyKeys ( O, Type ) +bool js::GetOwnPropertyKeys(JSContext* cx, HandleObject obj, unsigned flags, + MutableHandleValue rval) { + // Step 1 (Performed in caller). + + // Steps 2-4. + RootedIdVector keys(cx); + if (!GetPropertyKeys(cx, obj, flags, &keys)) { + return false; + } + + // Step 5 (Inlined CreateArrayFromList). + Rooted<ArrayObject*> array(cx, + NewDenseFullyAllocatedArray(cx, keys.length())); + if (!array) { + return false; + } + + array->ensureDenseInitializedLength(0, keys.length()); + + RootedValue val(cx); + for (size_t i = 0, len = keys.length(); i < len; i++) { + MOZ_ASSERT_IF(keys[i].isSymbol(), flags & JSITER_SYMBOLS); + MOZ_ASSERT_IF(!keys[i].isSymbol(), !(flags & JSITER_SYMBOLSONLY)); + if (!IdToStringOrSymbol(cx, keys[i], &val)) { + return false; + } + array->initDenseElement(i, val); + } + + rval.setObject(*array); + return true; +} + +// ES2018 draft rev c164be80f7ea91de5526b33d54e5c9321ed03d3f +// 19.1.2.9 Object.getOwnPropertyNames ( O ) +static bool obj_getOwnPropertyNames(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Object", "getOwnPropertyNames"); + CallArgs args = CallArgsFromVp(argc, vp); + + RootedObject obj(cx, ToObject(cx, args.get(0))); + if (!obj) { + return false; + } + + bool optimized; + static constexpr EnumerableOwnPropertiesKind kind = + EnumerableOwnPropertiesKind::Names; + if (!TryEnumerableOwnPropertiesNative<kind>(cx, obj, args.rval(), + &optimized)) { + return false; + } + if (optimized) { + return true; + } + + return GetOwnPropertyKeys(cx, obj, JSITER_OWNONLY | JSITER_HIDDEN, + args.rval()); +} + +// ES2018 draft rev c164be80f7ea91de5526b33d54e5c9321ed03d3f +// 19.1.2.10 Object.getOwnPropertySymbols ( O ) +static bool obj_getOwnPropertySymbols(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Object", "getOwnPropertySymbols"); + CallArgs args = CallArgsFromVp(argc, vp); + + RootedObject obj(cx, ToObject(cx, args.get(0))); + if (!obj) { + return false; + } + + return GetOwnPropertyKeys( + cx, obj, + JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS | JSITER_SYMBOLSONLY, + args.rval()); +} + +/* ES5 15.2.3.7: Object.defineProperties(O, Properties) */ +static bool obj_defineProperties(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Object", "defineProperties"); + CallArgs args = CallArgsFromVp(argc, vp); + + /* Step 1. */ + RootedObject obj(cx); + if (!GetFirstArgumentAsObject(cx, args, "Object.defineProperties", &obj)) { + return false; + } + + /* Step 2. */ + if (!args.requireAtLeast(cx, "Object.defineProperties", 2)) { + return false; + } + + /* Steps 3-6. */ + bool failedOnWindowProxy = false; + if (!ObjectDefineProperties(cx, obj, args[1], &failedOnWindowProxy)) { + return false; + } + + /* Step 7, but modified to deal with WindowProxy mess */ + if (failedOnWindowProxy) { + args.rval().setNull(); + } else { + args.rval().setObject(*obj); + } + return true; +} + +// ES6 20141014 draft 19.1.2.15 Object.preventExtensions(O) +static bool obj_preventExtensions(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().set(args.get(0)); + + // Step 1. + if (!args.get(0).isObject()) { + return true; + } + + // Steps 2-5. + RootedObject obj(cx, &args.get(0).toObject()); + return PreventExtensions(cx, obj); +} + +// ES6 draft rev27 (2014/08/24) 19.1.2.5 Object.freeze(O) +static bool obj_freeze(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().set(args.get(0)); + + // Step 1. + if (!args.get(0).isObject()) { + return true; + } + + // Steps 2-5. + RootedObject obj(cx, &args.get(0).toObject()); + return SetIntegrityLevel(cx, obj, IntegrityLevel::Frozen); +} + +// ES6 draft rev27 (2014/08/24) 19.1.2.12 Object.isFrozen(O) +static bool obj_isFrozen(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + bool frozen = true; + + // Step 2. + if (args.get(0).isObject()) { + RootedObject obj(cx, &args.get(0).toObject()); + if (!TestIntegrityLevel(cx, obj, IntegrityLevel::Frozen, &frozen)) { + return false; + } + } + args.rval().setBoolean(frozen); + return true; +} + +// ES6 draft rev27 (2014/08/24) 19.1.2.17 Object.seal(O) +static bool obj_seal(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().set(args.get(0)); + + // Step 1. + if (!args.get(0).isObject()) { + return true; + } + + // Steps 2-5. + RootedObject obj(cx, &args.get(0).toObject()); + return SetIntegrityLevel(cx, obj, IntegrityLevel::Sealed); +} + +// ES6 draft rev27 (2014/08/24) 19.1.2.13 Object.isSealed(O) +static bool obj_isSealed(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + bool sealed = true; + + // Step 2. + if (args.get(0).isObject()) { + RootedObject obj(cx, &args.get(0).toObject()); + if (!TestIntegrityLevel(cx, obj, IntegrityLevel::Sealed, &sealed)) { + return false; + } + } + args.rval().setBoolean(sealed); + return true; +} + +bool js::obj_setProto(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + HandleValue thisv = args.thisv(); + if (thisv.isNullOrUndefined()) { + ReportIncompatible(cx, args); + return false; + } + if (thisv.isPrimitive()) { + // Mutating a boxed primitive's [[Prototype]] has no side effects. + args.rval().setUndefined(); + return true; + } + + /* Do nothing if __proto__ isn't being set to an object or null. */ + if (!args[0].isObjectOrNull()) { + args.rval().setUndefined(); + return true; + } + + Rooted<JSObject*> obj(cx, &args.thisv().toObject()); + Rooted<JSObject*> newProto(cx, args[0].toObjectOrNull()); + if (!SetPrototype(cx, obj, newProto)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static const JSFunctionSpec object_methods[] = { + JS_FN(js_toSource_str, obj_toSource, 0, 0), + JS_INLINABLE_FN(js_toString_str, obj_toString, 0, 0, ObjectToString), + JS_SELF_HOSTED_FN(js_toLocaleString_str, "Object_toLocaleString", 0, 0), + JS_SELF_HOSTED_FN(js_valueOf_str, "Object_valueOf", 0, 0), + JS_SELF_HOSTED_FN(js_hasOwnProperty_str, "Object_hasOwnProperty", 1, 0), + JS_INLINABLE_FN(js_isPrototypeOf_str, obj_isPrototypeOf, 1, 0, + ObjectIsPrototypeOf), + JS_FN(js_propertyIsEnumerable_str, obj_propertyIsEnumerable, 1, 0), + JS_SELF_HOSTED_FN(js_defineGetter_str, "ObjectDefineGetter", 2, 0), + JS_SELF_HOSTED_FN(js_defineSetter_str, "ObjectDefineSetter", 2, 0), + JS_SELF_HOSTED_FN(js_lookupGetter_str, "ObjectLookupGetter", 1, 0), + JS_SELF_HOSTED_FN(js_lookupSetter_str, "ObjectLookupSetter", 1, 0), + JS_FS_END}; + +static const JSPropertySpec object_properties[] = { + JS_SELF_HOSTED_GETSET("__proto__", "$ObjectProtoGetter", + "$ObjectProtoSetter", 0), + JS_PS_END}; + +static const JSFunctionSpec object_static_methods[] = { + JS_FN("assign", obj_assign, 2, 0), + JS_SELF_HOSTED_FN("getPrototypeOf", "ObjectGetPrototypeOf", 1, 0), + JS_FN("setPrototypeOf", obj_setPrototypeOf, 2, 0), + JS_SELF_HOSTED_FN("getOwnPropertyDescriptor", + "ObjectGetOwnPropertyDescriptor", 2, 0), + JS_SELF_HOSTED_FN("getOwnPropertyDescriptors", + "ObjectGetOwnPropertyDescriptors", 1, 0), + JS_FN("keys", obj_keys, 1, 0), + JS_FN("values", obj_values, 1, 0), + JS_FN("entries", obj_entries, 1, 0), + JS_INLINABLE_FN("is", obj_is, 2, 0, ObjectIs), + JS_SELF_HOSTED_FN("defineProperty", "ObjectDefineProperty", 3, 0), + JS_FN("defineProperties", obj_defineProperties, 2, 0), + JS_INLINABLE_FN("create", obj_create, 2, 0, ObjectCreate), + JS_FN("getOwnPropertyNames", obj_getOwnPropertyNames, 1, 0), + JS_FN("getOwnPropertySymbols", obj_getOwnPropertySymbols, 1, 0), + JS_SELF_HOSTED_FN("isExtensible", "ObjectIsExtensible", 1, 0), + JS_FN("preventExtensions", obj_preventExtensions, 1, 0), + JS_FN("freeze", obj_freeze, 1, 0), + JS_FN("isFrozen", obj_isFrozen, 1, 0), + JS_FN("seal", obj_seal, 1, 0), + JS_FN("isSealed", obj_isSealed, 1, 0), + JS_SELF_HOSTED_FN("fromEntries", "ObjectFromEntries", 1, 0), + JS_SELF_HOSTED_FN("hasOwn", "ObjectHasOwn", 2, 0), + JS_FS_END}; + +static JSObject* CreateObjectConstructor(JSContext* cx, JSProtoKey key) { + Rooted<GlobalObject*> self(cx, cx->global()); + if (!GlobalObject::ensureConstructor(cx, self, JSProto_Function)) { + return nullptr; + } + + /* Create the Object function now that we have a [[Prototype]] for it. */ + JSFunction* fun = NewNativeConstructor( + cx, obj_construct, 1, Handle<PropertyName*>(cx->names().Object), + gc::AllocKind::FUNCTION, TenuredObject); + if (!fun) { + return nullptr; + } + + fun->setJitInfo(&jit::JitInfo_Object); + return fun; +} + +static JSObject* CreateObjectPrototype(JSContext* cx, JSProtoKey key) { + MOZ_ASSERT(!cx->zone()->isAtomsZone()); + MOZ_ASSERT(cx->global()->is<NativeObject>()); + + /* + * Create |Object.prototype| first, mirroring CreateBlankProto but for the + * prototype of the created object. + */ + Rooted<PlainObject*> objectProto( + cx, NewPlainObjectWithProto(cx, nullptr, TenuredObject)); + if (!objectProto) { + return nullptr; + } + + bool succeeded; + if (!SetImmutablePrototype(cx, objectProto, &succeeded)) { + return nullptr; + } + MOZ_ASSERT(succeeded, + "should have been able to make a fresh Object.prototype's " + "[[Prototype]] immutable"); + + return objectProto; +} + +static bool FinishObjectClassInit(JSContext* cx, JS::HandleObject ctor, + JS::HandleObject proto) { + Rooted<GlobalObject*> global(cx, cx->global()); + + // ES5 15.1.2.1. + RootedId evalId(cx, NameToId(cx->names().eval)); + JSFunction* evalobj = + DefineFunction(cx, global, evalId, IndirectEval, 1, JSPROP_RESOLVING); + if (!evalobj) { + return false; + } + global->setOriginalEval(evalobj); + +#ifdef FUZZING + if (cx->options().fuzzing()) { + if (!DefineTestingFunctions(cx, global, /* fuzzingSafe = */ true, + /* disableOOMFunctions = */ false)) { + return false; + } + } +#endif + + // The global object should have |Object.prototype| as its [[Prototype]]. + MOZ_ASSERT(global->staticPrototype() == nullptr); + MOZ_ASSERT(!global->staticPrototypeIsImmutable()); + return SetPrototype(cx, global, proto); +} + +static const ClassSpec PlainObjectClassSpec = { + CreateObjectConstructor, CreateObjectPrototype, + object_static_methods, nullptr, + object_methods, object_properties, + FinishObjectClassInit}; + +const JSClass PlainObject::class_ = {js_Object_str, + JSCLASS_HAS_CACHED_PROTO(JSProto_Object), + JS_NULL_CLASS_OPS, &PlainObjectClassSpec}; + +const JSClass* const js::ObjectClassPtr = &PlainObject::class_; diff --git a/js/src/builtin/Object.h b/js/src/builtin/Object.h new file mode 100644 index 0000000000..6aebb8ce85 --- /dev/null +++ b/js/src/builtin/Object.h @@ -0,0 +1,66 @@ +/* -*- 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 builtin_Object_h +#define builtin_Object_h + +#include "vm/JSObject.h" + +namespace JS { +class Value; +} + +namespace js { + +class PlainObject; + +// Object constructor native. Exposed only so the JIT can know its address. +[[nodiscard]] bool obj_construct(JSContext* cx, unsigned argc, JS::Value* vp); + +PlainObject* ObjectCreateImpl(JSContext* cx, HandleObject proto, + NewObjectKind newKind = GenericObject); + +PlainObject* ObjectCreateWithTemplate(JSContext* cx, + Handle<PlainObject*> templateObj); + +// Object methods exposed so they can be installed in the self-hosting global. +[[nodiscard]] bool obj_propertyIsEnumerable(JSContext* cx, unsigned argc, + Value* vp); + +[[nodiscard]] bool obj_isPrototypeOf(JSContext* cx, unsigned argc, Value* vp); + +[[nodiscard]] bool obj_create(JSContext* cx, unsigned argc, JS::Value* vp); + +[[nodiscard]] bool obj_is(JSContext* cx, unsigned argc, JS::Value* vp); + +[[nodiscard]] bool obj_toString(JSContext* cx, unsigned argc, JS::Value* vp); + +[[nodiscard]] bool obj_setProto(JSContext* cx, unsigned argc, JS::Value* vp); + +JSString* ObjectClassToString(JSContext* cx, JSObject* obj); + +[[nodiscard]] bool GetOwnPropertyKeys(JSContext* cx, HandleObject obj, + unsigned flags, + JS::MutableHandleValue rval); + +// Exposed for SelfHosting.cpp +[[nodiscard]] bool GetOwnPropertyDescriptorToArray(JSContext* cx, unsigned argc, + JS::Value* vp); + +/* + * Like IdToValue, but convert int jsids to strings. This is used when + * exposing a jsid to script for Object.getOwnProperty{Names,Symbols} + * or scriptable proxy traps. + */ +[[nodiscard]] bool IdToStringOrSymbol(JSContext* cx, JS::HandleId id, + JS::MutableHandleValue result); + +// Object.prototype.toSource. Function.prototype.toSource and uneval use this. +JSString* ObjectToSource(JSContext* cx, JS::HandleObject obj); + +} /* namespace js */ + +#endif /* builtin_Object_h */ diff --git a/js/src/builtin/Object.js b/js/src/builtin/Object.js new file mode 100644 index 0000000000..6193f1125c --- /dev/null +++ b/js/src/builtin/Object.js @@ -0,0 +1,369 @@ +/* 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/. */ + +// ES stage 4 proposal +function ObjectGetOwnPropertyDescriptors(O) { + // Step 1. + var obj = ToObject(O); + + // Step 2. + var keys = std_Reflect_ownKeys(obj); + + // Step 3. + var descriptors = {}; + + // Step 4. + for (var index = 0, len = keys.length; index < len; index++) { + var key = keys[index]; + + // Steps 4.a-b. + var desc = ObjectGetOwnPropertyDescriptor(obj, key); + + // Step 4.c. + if (typeof desc !== "undefined") { + DefineDataProperty(descriptors, key, desc); + } + } + + // Step 5. + return descriptors; +} + +/* ES6 draft rev 32 (2015 Feb 2) 19.1.2.9. */ +function ObjectGetPrototypeOf(obj) { + return std_Reflect_getPrototypeOf(ToObject(obj)); +} + +/* ES6 draft rev 32 (2015 Feb 2) 19.1.2.11. */ +function ObjectIsExtensible(obj) { + return IsObject(obj) && std_Reflect_isExtensible(obj); +} + +/* ES2015 19.1.3.5 Object.prototype.toLocaleString */ +function Object_toLocaleString() { + // Step 1. + var O = this; + + // Step 2. + return callContentFunction(O.toString, O); +} + +// ES 2017 draft bb96899bb0d9ef9be08164a26efae2ee5f25e875 19.1.3.7 +function Object_valueOf() { + // Step 1. + return ToObject(this); +} + +// ES 2018 draft 19.1.3.2 +function Object_hasOwnProperty(V) { + // Implement hasOwnProperty as a pseudo function that becomes a JSOp + // to easier add an inline cache for this. + return hasOwn(V, this); +} + +// ES 2021 draft rev 0b988b7700de675331ac360d164c978d6ea452ec +// B.2.2.1.1 get Object.prototype.__proto__ +function $ObjectProtoGetter() { + return std_Reflect_getPrototypeOf(ToObject(this)); +} +SetCanonicalName($ObjectProtoGetter, "get __proto__"); + +// ES 2021 draft rev 0b988b7700de675331ac360d164c978d6ea452ec +// B.2.2.1.2 set Object.prototype.__proto__ +function $ObjectProtoSetter(proto) { + return callFunction(std_Object_setProto, this, proto); +} +SetCanonicalName($ObjectProtoSetter, "set __proto__"); + +// ES7 draft (2016 March 8) B.2.2.3 +function ObjectDefineSetter(name, setter) { + // Step 1. + var object = ToObject(this); + + // Step 2. + if (!IsCallable(setter)) { + ThrowTypeError(JSMSG_BAD_GETTER_OR_SETTER, "setter"); + } + + // Step 4. + var key = TO_PROPERTY_KEY(name); + + // Steps 3, 5. + DefineProperty( + object, + key, + ACCESSOR_DESCRIPTOR_KIND | ATTR_ENUMERABLE | ATTR_CONFIGURABLE, + null, + setter, + true + ); + + // Step 6. (implicit) +} + +// ES7 draft (2016 March 8) B.2.2.2 +function ObjectDefineGetter(name, getter) { + // Step 1. + var object = ToObject(this); + + // Step 2. + if (!IsCallable(getter)) { + ThrowTypeError(JSMSG_BAD_GETTER_OR_SETTER, "getter"); + } + + // Step 4. + var key = TO_PROPERTY_KEY(name); + + // Steps 3, 5. + DefineProperty( + object, + key, + ACCESSOR_DESCRIPTOR_KIND | ATTR_ENUMERABLE | ATTR_CONFIGURABLE, + getter, + null, + true + ); + + // Step 6. (implicit) +} + +// ES7 draft (2016 March 8) B.2.2.5 +function ObjectLookupSetter(name) { + // Step 1. + var object = ToObject(this); + + // Step 2. + var key = TO_PROPERTY_KEY(name); + + do { + // Step 3.a. + var desc = GetOwnPropertyDescriptorToArray(object, key); + + // Step 3.b. + if (desc) { + // Step.b.i. + if (desc[PROP_DESC_ATTRS_AND_KIND_INDEX] & ACCESSOR_DESCRIPTOR_KIND) { + return desc[PROP_DESC_SETTER_INDEX]; + } + + // Step.b.i. + return undefined; + } + + // Step 3.c. + object = std_Reflect_getPrototypeOf(object); + } while (object !== null); + + // Step 3.d. (implicit) +} + +// ES7 draft (2016 March 8) B.2.2.4 +function ObjectLookupGetter(name) { + // Step 1. + var object = ToObject(this); + + // Step 2. + var key = TO_PROPERTY_KEY(name); + + do { + // Step 3.a. + var desc = GetOwnPropertyDescriptorToArray(object, key); + + // Step 3.b. + if (desc) { + // Step.b.i. + if (desc[PROP_DESC_ATTRS_AND_KIND_INDEX] & ACCESSOR_DESCRIPTOR_KIND) { + return desc[PROP_DESC_GETTER_INDEX]; + } + + // Step.b.ii. + return undefined; + } + + // Step 3.c. + object = std_Reflect_getPrototypeOf(object); + } while (object !== null); + + // Step 3.d. (implicit) +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 19.1.2.6 Object.getOwnPropertyDescriptor ( O, P ) +function ObjectGetOwnPropertyDescriptor(obj, propertyKey) { + // Steps 1-3. + var desc = GetOwnPropertyDescriptorToArray(obj, propertyKey); + + // Step 4 (Call to 6.2.4.4 FromPropertyDescriptor). + + // 6.2.4.4 FromPropertyDescriptor, step 1. + if (!desc) { + return undefined; + } + + // 6.2.4.4 FromPropertyDescriptor, steps 2-5, 8-11. + var attrsAndKind = desc[PROP_DESC_ATTRS_AND_KIND_INDEX]; + if (attrsAndKind & DATA_DESCRIPTOR_KIND) { + return { + value: desc[PROP_DESC_VALUE_INDEX], + writable: !!(attrsAndKind & ATTR_WRITABLE), + enumerable: !!(attrsAndKind & ATTR_ENUMERABLE), + configurable: !!(attrsAndKind & ATTR_CONFIGURABLE), + }; + } + + // 6.2.4.4 FromPropertyDescriptor, steps 2-3, 6-11. + assert( + attrsAndKind & ACCESSOR_DESCRIPTOR_KIND, + "expected accessor property descriptor" + ); + return { + get: desc[PROP_DESC_GETTER_INDEX], + set: desc[PROP_DESC_SETTER_INDEX], + enumerable: !!(attrsAndKind & ATTR_ENUMERABLE), + configurable: !!(attrsAndKind & ATTR_CONFIGURABLE), + }; +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 19.1.2.4 Object.defineProperty ( O, P, Attributes ) +// 26.1.3 Reflect.defineProperty ( target, propertyKey, attributes ) +function ObjectOrReflectDefineProperty(obj, propertyKey, attributes, strict) { + // Step 1. + if (!IsObject(obj)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, DecompileArg(0, obj)); + } + + // Step 2. + propertyKey = TO_PROPERTY_KEY(propertyKey); + + // Step 3 (Call to 6.2.4.5 ToPropertyDescriptor). + + // 6.2.4.5 ToPropertyDescriptor, step 1. + if (!IsObject(attributes)) { + ThrowTypeError( + JSMSG_OBJECT_REQUIRED_PROP_DESC, + DecompileArg(2, attributes) + ); + } + + // 6.2.4.5 ToPropertyDescriptor, step 2. + var attrs = 0; + var hasValue = false; + var value; + var getter = null; + var setter = null; + + // 6.2.4.5 ToPropertyDescriptor, steps 3-4. + if ("enumerable" in attributes) { + attrs |= attributes.enumerable ? ATTR_ENUMERABLE : ATTR_NONENUMERABLE; + } + + // 6.2.4.5 ToPropertyDescriptor, steps 5-6. + if ("configurable" in attributes) { + attrs |= attributes.configurable ? ATTR_CONFIGURABLE : ATTR_NONCONFIGURABLE; + } + + // 6.2.4.5 ToPropertyDescriptor, steps 7-8. + if ("value" in attributes) { + attrs |= DATA_DESCRIPTOR_KIND; + value = attributes.value; + hasValue = true; + } + + // 6.2.4.5 ToPropertyDescriptor, steps 9-10. + if ("writable" in attributes) { + attrs |= DATA_DESCRIPTOR_KIND; + attrs |= attributes.writable ? ATTR_WRITABLE : ATTR_NONWRITABLE; + } + + // 6.2.4.5 ToPropertyDescriptor, steps 11-12. + if ("get" in attributes) { + attrs |= ACCESSOR_DESCRIPTOR_KIND; + getter = attributes.get; + if (!IsCallable(getter) && getter !== undefined) { + ThrowTypeError(JSMSG_BAD_GET_SET_FIELD, "get"); + } + } + + // 6.2.4.5 ToPropertyDescriptor, steps 13-14. + if ("set" in attributes) { + attrs |= ACCESSOR_DESCRIPTOR_KIND; + setter = attributes.set; + if (!IsCallable(setter) && setter !== undefined) { + ThrowTypeError(JSMSG_BAD_GET_SET_FIELD, "set"); + } + } + + if (attrs & ACCESSOR_DESCRIPTOR_KIND) { + // 6.2.4.5 ToPropertyDescriptor, step 15. + if (attrs & DATA_DESCRIPTOR_KIND) { + ThrowTypeError(JSMSG_INVALID_DESCRIPTOR); + } + + // Step 4 (accessor descriptor property). + return DefineProperty(obj, propertyKey, attrs, getter, setter, strict); + } + + // Step 4 (data property descriptor with value). + if (hasValue) { + // Use the inlinable DefineDataProperty function when possible. + if (strict) { + if ( + (attrs & (ATTR_ENUMERABLE | ATTR_CONFIGURABLE | ATTR_WRITABLE)) === + (ATTR_ENUMERABLE | ATTR_CONFIGURABLE | ATTR_WRITABLE) + ) { + DefineDataProperty(obj, propertyKey, value); + return true; + } + } + + // The fifth argument is set to |null| to mark that |value| is present. + return DefineProperty(obj, propertyKey, attrs, value, null, strict); + } + + // Step 4 (generic property descriptor or data property without value). + return DefineProperty(obj, propertyKey, attrs, undefined, undefined, strict); +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 19.1.2.4 Object.defineProperty ( O, P, Attributes ) +function ObjectDefineProperty(obj, propertyKey, attributes) { + // Steps 1-4. + if (!ObjectOrReflectDefineProperty(obj, propertyKey, attributes, true)) { + // Not standardized yet: https://github.com/tc39/ecma262/pull/688 + return null; + } + + // Step 5. + return obj; +} + +// Proposal https://tc39.github.io/proposal-object-from-entries/ +// 1. Object.fromEntries ( iterable ) +function ObjectFromEntries(iter) { + // We omit the usual step number comments here because they don't help. + // This implementation inlines AddEntriesFromIterator and + // CreateDataPropertyOnObject, so it looks more like the polyfill + // <https://github.com/tc39/proposal-object-from-entries/blob/master/polyfill.js> + // than the spec algorithm. + const obj = {}; + + for (const pair of allowContentIter(iter)) { + if (!IsObject(pair)) { + ThrowTypeError(JSMSG_INVALID_MAP_ITERABLE, "Object.fromEntries"); + } + DefineDataProperty(obj, pair[0], pair[1]); + } + + return obj; +} + +// Proposal https://github.com/tc39/proposal-accessible-object-hasownproperty +// 1. Object.hasOwn ( O, P ) +function ObjectHasOwn(O, P) { + // Step 1. + var obj = ToObject(O); + // Step 2-3. + return hasOwn(P, obj); +} diff --git a/js/src/builtin/Profilers.cpp b/js/src/builtin/Profilers.cpp new file mode 100644 index 0000000000..06a825e111 --- /dev/null +++ b/js/src/builtin/Profilers.cpp @@ -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/. */ + +/* Profiling-related API */ + +#include "builtin/Profilers.h" + +#include "mozilla/Compiler.h" +#include "mozilla/Sprintf.h" + +#include <iterator> +#include <stdarg.h> + +#include "util/GetPidProvider.h" // getpid() + +#ifdef MOZ_CALLGRIND +# include <valgrind/callgrind.h> +#endif + +#ifdef __APPLE__ +# ifdef MOZ_INSTRUMENTS +# include "devtools/Instruments.h" +# endif +#endif + +#include "js/CharacterEncoding.h" +#include "js/PropertyAndElement.h" // JS_DefineFunctions +#include "js/PropertySpec.h" +#include "js/Utility.h" +#include "util/Text.h" +#include "vm/Probes.h" + +#include "vm/JSContext-inl.h" + +using namespace js; + +/* Thread-unsafe error management */ + +static char gLastError[2000]; + +#if defined(__APPLE__) || defined(__linux__) || defined(MOZ_CALLGRIND) +static void MOZ_FORMAT_PRINTF(1, 2) UnsafeError(const char* format, ...) { + va_list args; + va_start(args, format); + (void)VsprintfLiteral(gLastError, format, args); + va_end(args); +} +#endif + +JS_PUBLIC_API const char* JS_UnsafeGetLastProfilingError() { + return gLastError; +} + +#ifdef __APPLE__ +static bool StartOSXProfiling(const char* profileName, pid_t pid) { + bool ok = true; + const char* profiler = nullptr; +# ifdef MOZ_INSTRUMENTS + ok = Instruments::Start(pid); + profiler = "Instruments"; +# endif + if (!ok) { + if (profileName) { + UnsafeError("Failed to start %s for %s", profiler, profileName); + } else { + UnsafeError("Failed to start %s", profiler); + } + return false; + } + return true; +} +#endif + +JS_PUBLIC_API bool JS_StartProfiling(const char* profileName, pid_t pid) { + bool ok = true; +#ifdef __APPLE__ + ok = StartOSXProfiling(profileName, pid); +#endif +#ifdef __linux__ + if (!js_StartPerf()) { + ok = false; + } +#endif + return ok; +} + +JS_PUBLIC_API bool JS_StopProfiling(const char* profileName) { + bool ok = true; +#ifdef __APPLE__ +# ifdef MOZ_INSTRUMENTS + Instruments::Stop(profileName); +# endif +#endif +#ifdef __linux__ + if (!js_StopPerf()) { + ok = false; + } +#endif + return ok; +} + +/* + * Start or stop whatever platform- and configuration-specific profiling + * backends are available. + */ +static bool ControlProfilers(bool toState) { + bool ok = true; + + if (!probes::ProfilingActive && toState) { +#ifdef __APPLE__ +# if defined(MOZ_INSTRUMENTS) + const char* profiler; +# ifdef MOZ_INSTRUMENTS + ok = Instruments::Resume(); + profiler = "Instruments"; +# endif + if (!ok) { + UnsafeError("Failed to start %s", profiler); + } +# endif +#endif +#ifdef MOZ_CALLGRIND + if (!js_StartCallgrind()) { + UnsafeError("Failed to start Callgrind"); + ok = false; + } +#endif + } else if (probes::ProfilingActive && !toState) { +#ifdef __APPLE__ +# ifdef MOZ_INSTRUMENTS + Instruments::Pause(); +# endif +#endif +#ifdef MOZ_CALLGRIND + if (!js_StopCallgrind()) { + UnsafeError("failed to stop Callgrind"); + ok = false; + } +#endif + } + + probes::ProfilingActive = toState; + + return ok; +} + +/* + * Pause/resume whatever profiling mechanism is currently compiled + * in, if applicable. This will not affect things like dtrace. + * + * Do not mix calls to these APIs with calls to the individual + * profilers' pause/resume functions, because only overall state is + * tracked, not the state of each profiler. + */ +JS_PUBLIC_API bool JS_PauseProfilers(const char* profileName) { + return ControlProfilers(false); +} + +JS_PUBLIC_API bool JS_ResumeProfilers(const char* profileName) { + return ControlProfilers(true); +} + +JS_PUBLIC_API bool JS_DumpProfile(const char* outfile, + const char* profileName) { + bool ok = true; +#ifdef MOZ_CALLGRIND + ok = js_DumpCallgrind(outfile); +#endif + return ok; +} + +#ifdef MOZ_PROFILING + +static UniqueChars RequiredStringArg(JSContext* cx, const CallArgs& args, + size_t argi, const char* caller) { + if (args.length() <= argi) { + JS_ReportErrorASCII(cx, "%s: not enough arguments", caller); + return nullptr; + } + + if (!args[argi].isString()) { + JS_ReportErrorASCII(cx, "%s: invalid arguments (string expected)", caller); + return nullptr; + } + + return JS_EncodeStringToLatin1(cx, args[argi].toString()); +} + +static bool StartProfiling(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() == 0) { + args.rval().setBoolean(JS_StartProfiling(nullptr, getpid())); + return true; + } + + UniqueChars profileName = RequiredStringArg(cx, args, 0, "startProfiling"); + if (!profileName) { + return false; + } + + if (args.length() == 1) { + args.rval().setBoolean(JS_StartProfiling(profileName.get(), getpid())); + return true; + } + + if (!args[1].isInt32()) { + JS_ReportErrorASCII(cx, "startProfiling: invalid arguments (int expected)"); + return false; + } + pid_t pid = static_cast<pid_t>(args[1].toInt32()); + args.rval().setBoolean(JS_StartProfiling(profileName.get(), pid)); + return true; +} + +static bool StopProfiling(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() == 0) { + args.rval().setBoolean(JS_StopProfiling(nullptr)); + return true; + } + + UniqueChars profileName = RequiredStringArg(cx, args, 0, "stopProfiling"); + if (!profileName) { + return false; + } + args.rval().setBoolean(JS_StopProfiling(profileName.get())); + return true; +} + +static bool PauseProfilers(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() == 0) { + args.rval().setBoolean(JS_PauseProfilers(nullptr)); + return true; + } + + UniqueChars profileName = RequiredStringArg(cx, args, 0, "pauseProfiling"); + if (!profileName) { + return false; + } + args.rval().setBoolean(JS_PauseProfilers(profileName.get())); + return true; +} + +static bool ResumeProfilers(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() == 0) { + args.rval().setBoolean(JS_ResumeProfilers(nullptr)); + return true; + } + + UniqueChars profileName = RequiredStringArg(cx, args, 0, "resumeProfiling"); + if (!profileName) { + return false; + } + args.rval().setBoolean(JS_ResumeProfilers(profileName.get())); + return true; +} + +/* Usage: DumpProfile([filename[, profileName]]) */ +static bool DumpProfile(JSContext* cx, unsigned argc, Value* vp) { + bool ret; + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() == 0) { + ret = JS_DumpProfile(nullptr, nullptr); + } else { + UniqueChars filename = RequiredStringArg(cx, args, 0, "dumpProfile"); + if (!filename) { + return false; + } + + if (args.length() == 1) { + ret = JS_DumpProfile(filename.get(), nullptr); + } else { + UniqueChars profileName = RequiredStringArg(cx, args, 1, "dumpProfile"); + if (!profileName) { + return false; + } + + ret = JS_DumpProfile(filename.get(), profileName.get()); + } + } + + args.rval().setBoolean(ret); + return true; +} + +static bool GetMaxGCPauseSinceClear(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setNumber( + cx->runtime()->gc.stats().getMaxGCPauseSinceClear().ToMicroseconds()); + return true; +} + +static bool ClearMaxGCPauseAccumulator(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setNumber( + cx->runtime()->gc.stats().clearMaxGCPauseAccumulator().ToMicroseconds()); + return true; +} + +# if defined(MOZ_INSTRUMENTS) + +static bool IgnoreAndReturnTrue(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(true); + return true; +} + +# endif + +# ifdef MOZ_CALLGRIND +static bool StartCallgrind(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(js_StartCallgrind()); + return true; +} + +static bool StopCallgrind(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(js_StopCallgrind()); + return true; +} + +static bool DumpCallgrind(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() == 0) { + args.rval().setBoolean(js_DumpCallgrind(nullptr)); + return true; + } + + UniqueChars outFile = RequiredStringArg(cx, args, 0, "dumpCallgrind"); + if (!outFile) { + return false; + } + + args.rval().setBoolean(js_DumpCallgrind(outFile.get())); + return true; +} +# endif + +static const JSFunctionSpec profiling_functions[] = { + JS_FN("startProfiling", StartProfiling, 1, 0), + JS_FN("stopProfiling", StopProfiling, 1, 0), + JS_FN("pauseProfilers", PauseProfilers, 1, 0), + JS_FN("resumeProfilers", ResumeProfilers, 1, 0), + JS_FN("dumpProfile", DumpProfile, 2, 0), + JS_FN("getMaxGCPauseSinceClear", GetMaxGCPauseSinceClear, 0, 0), + JS_FN("clearMaxGCPauseAccumulator", ClearMaxGCPauseAccumulator, 0, 0), +# if defined(MOZ_INSTRUMENTS) + /* Keep users of the old shark API happy. */ + JS_FN("connectShark", IgnoreAndReturnTrue, 0, 0), + JS_FN("disconnectShark", IgnoreAndReturnTrue, 0, 0), + JS_FN("startShark", StartProfiling, 0, 0), + JS_FN("stopShark", StopProfiling, 0, 0), +# endif +# ifdef MOZ_CALLGRIND + JS_FN("startCallgrind", StartCallgrind, 0, 0), + JS_FN("stopCallgrind", StopCallgrind, 0, 0), + JS_FN("dumpCallgrind", DumpCallgrind, 1, 0), +# endif + JS_FS_END}; + +#endif + +JS_PUBLIC_API bool JS_DefineProfilingFunctions(JSContext* cx, + HandleObject obj) { + cx->check(obj); +#ifdef MOZ_PROFILING + return JS_DefineFunctions(cx, obj, profiling_functions); +#else + return true; +#endif +} + +#ifdef MOZ_CALLGRIND + +/* Wrapper for various macros to stop warnings coming from their expansions. */ +# if defined(__clang__) +# define JS_SILENCE_UNUSED_VALUE_IN_EXPR(expr) \ + JS_BEGIN_MACRO \ + _Pragma("clang diagnostic push") /* If these _Pragmas cause warnings \ + for you, try disabling ccache. */ \ + _Pragma("clang diagnostic ignored \"-Wunused-value\"") { \ + expr; \ + } \ + _Pragma("clang diagnostic pop") \ + JS_END_MACRO +# elif MOZ_IS_GCC + +# define JS_SILENCE_UNUSED_VALUE_IN_EXPR(expr) \ + JS_BEGIN_MACRO \ + _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic ignored \"-Wunused-but-set-variable\"") \ + expr; \ + _Pragma("GCC diagnostic pop") \ + JS_END_MACRO +# endif + +# if !defined(JS_SILENCE_UNUSED_VALUE_IN_EXPR) +# define JS_SILENCE_UNUSED_VALUE_IN_EXPR(expr) \ + JS_BEGIN_MACRO \ + expr; \ + JS_END_MACRO +# endif + +JS_PUBLIC_API bool js_StartCallgrind() { + JS_SILENCE_UNUSED_VALUE_IN_EXPR(CALLGRIND_START_INSTRUMENTATION); + JS_SILENCE_UNUSED_VALUE_IN_EXPR(CALLGRIND_ZERO_STATS); + return true; +} + +JS_PUBLIC_API bool js_StopCallgrind() { + JS_SILENCE_UNUSED_VALUE_IN_EXPR(CALLGRIND_STOP_INSTRUMENTATION); + return true; +} + +JS_PUBLIC_API bool js_DumpCallgrind(const char* outfile) { + if (outfile) { + JS_SILENCE_UNUSED_VALUE_IN_EXPR(CALLGRIND_DUMP_STATS_AT(outfile)); + } else { + JS_SILENCE_UNUSED_VALUE_IN_EXPR(CALLGRIND_DUMP_STATS); + } + + return true; +} + +#endif /* MOZ_CALLGRIND */ + +#ifdef __linux__ + +/* + * Code for starting and stopping |perf|, the Linux profiler. + * + * Output from profiling is written to mozperf.data in your cwd. + * + * To enable, set MOZ_PROFILE_WITH_PERF=1 in your environment. + * + * To pass additional parameters to |perf record|, provide them in the + * MOZ_PROFILE_PERF_FLAGS environment variable. If this variable does not + * exist, we default it to "--call-graph". (If you don't want --call-graph but + * don't want to pass any other args, define MOZ_PROFILE_PERF_FLAGS to the empty + * string.) + * + * If you include --pid or --output in MOZ_PROFILE_PERF_FLAGS, you're just + * asking for trouble. + * + * Our split-on-spaces logic is lame, so don't expect MOZ_PROFILE_PERF_FLAGS to + * work if you pass an argument which includes a space (e.g. + * MOZ_PROFILE_PERF_FLAGS="-e 'foo bar'"). + */ + +# include <signal.h> +# include <sys/wait.h> +# include <unistd.h> + +static bool perfInitialized = false; +static pid_t perfPid = 0; + +bool js_StartPerf() { + const char* outfile = "mozperf.data"; + + if (perfPid != 0) { + UnsafeError("js_StartPerf: called while perf was already running!\n"); + return false; + } + + // Bail if MOZ_PROFILE_WITH_PERF is empty or undefined. + if (!getenv("MOZ_PROFILE_WITH_PERF") || + !strlen(getenv("MOZ_PROFILE_WITH_PERF"))) { + return true; + } + + /* + * Delete mozperf.data the first time through -- we're going to append to it + * later on, so we want it to be clean when we start out. + */ + if (!perfInitialized) { + perfInitialized = true; + unlink(outfile); + char cwd[4096]; + printf("Writing perf profiling data to %s/%s\n", getcwd(cwd, sizeof(cwd)), + outfile); + } + + pid_t mainPid = getpid(); + + pid_t childPid = fork(); + if (childPid == 0) { + /* perf record --pid $mainPID --output=$outfile $MOZ_PROFILE_PERF_FLAGS */ + + char mainPidStr[16]; + SprintfLiteral(mainPidStr, "%d", mainPid); + const char* defaultArgs[] = {"perf", "record", "--pid", + mainPidStr, "--output", outfile}; + + Vector<const char*, 0, SystemAllocPolicy> args; + if (!args.append(defaultArgs, std::size(defaultArgs))) { + return false; + } + + const char* flags = getenv("MOZ_PROFILE_PERF_FLAGS"); + if (!flags) { + flags = "--call-graph"; + } + + UniqueChars flags2 = DuplicateString(flags); + if (!flags2) { + return false; + } + + // Split |flags2| on spaces. + char* toksave; + char* tok = strtok_r(flags2.get(), " ", &toksave); + while (tok) { + if (!args.append(tok)) { + return false; + } + tok = strtok_r(nullptr, " ", &toksave); + } + + if (!args.append((char*)nullptr)) { + return false; + } + + execvp("perf", const_cast<char**>(args.begin())); + + /* Reached only if execlp fails. */ + fprintf(stderr, "Unable to start perf.\n"); + exit(1); + } + if (childPid > 0) { + perfPid = childPid; + + /* Give perf a chance to warm up. */ + usleep(500 * 1000); + return true; + } + UnsafeError("js_StartPerf: fork() failed\n"); + return false; +} + +bool js_StopPerf() { + if (perfPid == 0) { + UnsafeError("js_StopPerf: perf is not running.\n"); + return true; + } + + if (kill(perfPid, SIGINT)) { + UnsafeError("js_StopPerf: kill failed\n"); + + // Try to reap the process anyway. + waitpid(perfPid, nullptr, WNOHANG); + } else { + waitpid(perfPid, nullptr, 0); + } + + perfPid = 0; + return true; +} + +#endif /* __linux__ */ diff --git a/js/src/builtin/Profilers.h b/js/src/builtin/Profilers.h new file mode 100644 index 0000000000..ab6235e215 --- /dev/null +++ b/js/src/builtin/Profilers.h @@ -0,0 +1,88 @@ +/* -*- 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/. */ + +/* + * Functions for controlling profilers from within JS: Valgrind, Perf, etc + */ +#ifndef builtin_Profilers_h +#define builtin_Profilers_h + +#include "jstypes.h" + +#ifdef _MSC_VER +typedef int pid_t; +#else +# include <unistd.h> +#endif + +/** + * Start any profilers that are available and have been configured on for this + * platform. This is NOT thread safe. + * + * The profileName is used by some profilers to describe the current profiling + * run. It may be used for part of the filename of the output, but the + * specifics depend on the profiler. Many profilers will ignore it. Passing in + * nullptr is legal; some profilers may use it to output to stdout or similar. + * + * Returns true if no profilers fail to start. + */ +[[nodiscard]] extern JS_PUBLIC_API bool JS_StartProfiling( + const char* profileName, pid_t pid); + +/** + * Stop any profilers that were previously started with JS_StartProfiling. + * Returns true if no profilers fail to stop. + */ +[[nodiscard]] extern JS_PUBLIC_API bool JS_StopProfiling( + const char* profileName); + +/** + * Write the current profile data to the given file, if applicable to whatever + * profiler is being used. + */ +[[nodiscard]] extern JS_PUBLIC_API bool JS_DumpProfile(const char* outfile, + const char* profileName); + +/** + * Pause currently active profilers (only supported by some profilers). Returns + * whether any profilers failed to pause. (Profilers that do not support + * pause/resume do not count.) + */ +[[nodiscard]] extern JS_PUBLIC_API bool JS_PauseProfilers( + const char* profileName); + +/** + * Resume suspended profilers + */ +[[nodiscard]] extern JS_PUBLIC_API bool JS_ResumeProfilers( + const char* profileName); + +/** + * The profiling API calls are not able to report errors, so they use a + * thread-unsafe global memory buffer to hold the last error encountered. This + * should only be called after something returns false. + */ +JS_PUBLIC_API const char* JS_UnsafeGetLastProfilingError(); + +#ifdef MOZ_CALLGRIND + +[[nodiscard]] extern JS_PUBLIC_API bool js_StopCallgrind(); + +[[nodiscard]] extern JS_PUBLIC_API bool js_StartCallgrind(); + +[[nodiscard]] extern JS_PUBLIC_API bool js_DumpCallgrind(const char* outfile); + +#endif /* MOZ_CALLGRIND */ + +#ifdef __linux__ + +[[nodiscard]] extern JS_PUBLIC_API bool js_StartPerf(); + +[[nodiscard]] extern JS_PUBLIC_API bool js_StopPerf(); + +#endif /* __linux__ */ + +#endif /* builtin_Profilers_h */ diff --git a/js/src/builtin/Promise-inl.h b/js/src/builtin/Promise-inl.h new file mode 100644 index 0000000000..1a5380db83 --- /dev/null +++ b/js/src/builtin/Promise-inl.h @@ -0,0 +1,45 @@ +/* -*- 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 builtin_Promise_inl_h +#define builtin_Promise_inl_h + +#include "js/Promise.h" // JS::PromiseState + +#include "mozilla/Assertions.h" // MOZ_ASSERT + +#include "js/RootingAPI.h" // JS::Handle +#include "vm/JSContext.h" // JSContext +#include "vm/PromiseObject.h" // js::PromiseObject + +namespace js { + +/** + * Given a settled (i.e. fulfilled or rejected, not pending) promise, sets + * |promise.[[PromiseIsHandled]]| to true and removes it from the list of + * unhandled rejected promises. + * + * NOTE: If you need to set |promise.[[PromiseIsHandled]]| on a pending promise, + * use |PromiseObject::setHandled()| directly. + */ +inline void SetSettledPromiseIsHandled( + JSContext* cx, JS::Handle<PromiseObject*> unwrappedPromise) { + MOZ_ASSERT(unwrappedPromise->state() != JS::PromiseState::Pending); + unwrappedPromise->setHandled(); + cx->runtime()->removeUnhandledRejectedPromise(cx, unwrappedPromise); +} + +inline void SetAnyPromiseIsHandled( + JSContext* cx, JS::Handle<PromiseObject*> unwrappedPromise) { + if (unwrappedPromise->state() != JS::PromiseState::Pending) { + cx->runtime()->removeUnhandledRejectedPromise(cx, unwrappedPromise); + } + unwrappedPromise->setHandled(); +} + +} // namespace js + +#endif // builtin_Promise_inl_h diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp new file mode 100644 index 0000000000..fa09eb292d --- /dev/null +++ b/js/src/builtin/Promise.cpp @@ -0,0 +1,6632 @@ +/* -*- 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 "builtin/Promise.h" + +#include "mozilla/Atomics.h" +#include "mozilla/Maybe.h" +#include "mozilla/TimeStamp.h" + +#include "jsapi.h" +#include "jsexn.h" +#include "jsfriendapi.h" + +#include "js/CallAndConstruct.h" // JS::Construct, JS::IsCallable +#include "js/experimental/JitInfo.h" // JSJitGetterOp, JSJitInfo +#include "js/ForOfIterator.h" // JS::ForOfIterator +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/PropertySpec.h" +#include "js/Stack.h" +#include "vm/ArrayObject.h" +#include "vm/AsyncFunction.h" +#include "vm/AsyncIteration.h" +#include "vm/CompletionKind.h" +#include "vm/ErrorObject.h" +#include "vm/ErrorReporting.h" +#include "vm/Iteration.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/PromiseLookup.h" // js::PromiseLookup +#include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseSlot_* +#include "vm/SelfHosting.h" +#include "vm/Warnings.h" // js::WarnNumberASCII + +#include "debugger/DebugAPI-inl.h" +#include "vm/Compartment-inl.h" +#include "vm/ErrorObject-inl.h" +#include "vm/JSContext-inl.h" // JSContext::check +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +static double MillisecondsSinceStartup() { + auto now = mozilla::TimeStamp::Now(); + return (now - mozilla::TimeStamp::FirstTimeStamp()).ToMilliseconds(); +} + +enum ResolutionMode { ResolveMode, RejectMode }; + +/** + * ES2023 draft rev 714fa3dd1e8237ae9c666146270f81880089eca5 + * + * Promise Resolve Functions + * https://tc39.es/ecma262/#sec-promise-resolve-functions + */ +enum ResolveFunctionSlots { + // NOTE: All slot represent [[AlreadyResolved]].[[Value]]. + // + // The spec creates single record for [[AlreadyResolved]] and shares it + // between Promise Resolve Function and Promise Reject Function. + // + // Step 1. Let alreadyResolved be the Record { [[Value]]: false }. + // ... + // Step 6. Set resolve.[[AlreadyResolved]] to alreadyResolved. + // ... + // Step 11. Set reject.[[AlreadyResolved]] to alreadyResolved. + // + // We implement it by clearing all slots, both in + // Promise Resolve Function and Promise Reject Function at the same time. + // + // If none of slots are undefined, [[AlreadyResolved]].[[Value]] is false. + // If all slot are undefined, [[AlreadyResolved]].[[Value]] is true. + + // [[Promise]] slot. + // A possibly-wrapped promise. + ResolveFunctionSlot_Promise = 0, + + // The corresponding Promise Reject Function. + ResolveFunctionSlot_RejectFunction, +}; + +/** + * ES2023 draft rev 714fa3dd1e8237ae9c666146270f81880089eca5 + * + * Promise Reject Functions + * https://tc39.es/ecma262/#sec-promise-reject-functions + */ +enum RejectFunctionSlots { + // [[Promise]] slot. + // A possibly-wrapped promise. + RejectFunctionSlot_Promise = 0, + + // The corresponding Promise Resolve Function. + RejectFunctionSlot_ResolveFunction, +}; + +enum PromiseCombinatorElementFunctionSlots { + PromiseCombinatorElementFunctionSlot_Data = 0, + PromiseCombinatorElementFunctionSlot_ElementIndex, +}; + +enum ReactionJobSlots { + ReactionJobSlot_ReactionRecord = 0, +}; + +enum ThenableJobSlots { + // The handler to use as the Promise reaction. It is a callable object + // that's guaranteed to be from the same compartment as the + // PromiseReactionJob. + ThenableJobSlot_Handler = 0, + + // JobData - a, potentially CCW-wrapped, dense list containing data + // required for proper execution of the reaction. + ThenableJobSlot_JobData, +}; + +enum ThenableJobDataIndices { + // The Promise to resolve using the given thenable. + ThenableJobDataIndex_Promise = 0, + + // The thenable to use as the receiver when calling the `then` function. + ThenableJobDataIndex_Thenable, + + ThenableJobDataLength, +}; + +enum BuiltinThenableJobSlots { + // The Promise to resolve using the given thenable. + BuiltinThenableJobSlot_Promise = 0, + + // The thenable to use as the receiver when calling the built-in `then` + // function. + BuiltinThenableJobSlot_Thenable, +}; + +struct PromiseCapability { + JSObject* promise = nullptr; + JSObject* resolve = nullptr; + JSObject* reject = nullptr; + + PromiseCapability() = default; + + void trace(JSTracer* trc); +}; + +void PromiseCapability::trace(JSTracer* trc) { + if (promise) { + TraceRoot(trc, &promise, "PromiseCapability::promise"); + } + if (resolve) { + TraceRoot(trc, &resolve, "PromiseCapability::resolve"); + } + if (reject) { + TraceRoot(trc, &reject, "PromiseCapability::reject"); + } +} + +namespace js { + +template <typename Wrapper> +class WrappedPtrOperations<PromiseCapability, Wrapper> { + const PromiseCapability& capability() const { + return static_cast<const Wrapper*>(this)->get(); + } + + public: + HandleObject promise() const { + return HandleObject::fromMarkedLocation(&capability().promise); + } + HandleObject resolve() const { + return HandleObject::fromMarkedLocation(&capability().resolve); + } + HandleObject reject() const { + return HandleObject::fromMarkedLocation(&capability().reject); + } +}; + +template <typename Wrapper> +class MutableWrappedPtrOperations<PromiseCapability, Wrapper> + : public WrappedPtrOperations<PromiseCapability, Wrapper> { + PromiseCapability& capability() { return static_cast<Wrapper*>(this)->get(); } + + public: + MutableHandleObject promise() { + return MutableHandleObject::fromMarkedLocation(&capability().promise); + } + MutableHandleObject resolve() { + return MutableHandleObject::fromMarkedLocation(&capability().resolve); + } + MutableHandleObject reject() { + return MutableHandleObject::fromMarkedLocation(&capability().reject); + } +}; + +} // namespace js + +struct PromiseCombinatorElements; + +class PromiseCombinatorDataHolder : public NativeObject { + enum { + Slot_Promise = 0, + Slot_RemainingElements, + Slot_ValuesArray, + Slot_ResolveOrRejectFunction, + SlotsCount, + }; + + public: + static const JSClass class_; + JSObject* promiseObj() { return &getFixedSlot(Slot_Promise).toObject(); } + JSObject* resolveOrRejectObj() { + return &getFixedSlot(Slot_ResolveOrRejectFunction).toObject(); + } + Value valuesArray() { return getFixedSlot(Slot_ValuesArray); } + int32_t remainingCount() { + return getFixedSlot(Slot_RemainingElements).toInt32(); + } + int32_t increaseRemainingCount() { + int32_t remainingCount = getFixedSlot(Slot_RemainingElements).toInt32(); + remainingCount++; + setFixedSlot(Slot_RemainingElements, Int32Value(remainingCount)); + return remainingCount; + } + int32_t decreaseRemainingCount() { + int32_t remainingCount = getFixedSlot(Slot_RemainingElements).toInt32(); + remainingCount--; + MOZ_ASSERT(remainingCount >= 0, "unpaired calls to decreaseRemainingCount"); + setFixedSlot(Slot_RemainingElements, Int32Value(remainingCount)); + return remainingCount; + } + + static PromiseCombinatorDataHolder* New( + JSContext* cx, HandleObject resultPromise, + Handle<PromiseCombinatorElements> elements, HandleObject resolveOrReject); +}; + +const JSClass PromiseCombinatorDataHolder::class_ = { + "PromiseCombinatorDataHolder", JSCLASS_HAS_RESERVED_SLOTS(SlotsCount)}; + +// Smart pointer to the "F.[[Values]]" part of the state of a Promise.all or +// Promise.allSettled invocation, or the "F.[[Errors]]" part of the state of a +// Promise.any invocation. Copes with compartment issues when setting an +// element. +struct MOZ_STACK_CLASS PromiseCombinatorElements final { + // Object value holding the elements array. The object can be a wrapper. + Value value; + + // Unwrapped elements array. May not belong to the current compartment! + ArrayObject* unwrappedArray = nullptr; + + // Set to true if the |setElement| method needs to wrap its input value. + bool setElementNeedsWrapping = false; + + PromiseCombinatorElements() = default; + + void trace(JSTracer* trc); +}; + +void PromiseCombinatorElements::trace(JSTracer* trc) { + TraceRoot(trc, &value, "PromiseCombinatorElements::value"); + if (unwrappedArray) { + TraceRoot(trc, &unwrappedArray, + "PromiseCombinatorElements::unwrappedArray"); + } +} + +namespace js { + +template <typename Wrapper> +class WrappedPtrOperations<PromiseCombinatorElements, Wrapper> { + const PromiseCombinatorElements& elements() const { + return static_cast<const Wrapper*>(this)->get(); + } + + public: + HandleValue value() const { + return HandleValue::fromMarkedLocation(&elements().value); + } + + Handle<ArrayObject*> unwrappedArray() const { + return Handle<ArrayObject*>::fromMarkedLocation(&elements().unwrappedArray); + } +}; + +template <typename Wrapper> +class MutableWrappedPtrOperations<PromiseCombinatorElements, Wrapper> + : public WrappedPtrOperations<PromiseCombinatorElements, Wrapper> { + PromiseCombinatorElements& elements() { + return static_cast<Wrapper*>(this)->get(); + } + + public: + MutableHandleValue value() { + return MutableHandleValue::fromMarkedLocation(&elements().value); + } + + MutableHandle<ArrayObject*> unwrappedArray() { + return MutableHandle<ArrayObject*>::fromMarkedLocation( + &elements().unwrappedArray); + } + + void initialize(ArrayObject* arrayObj) { + unwrappedArray().set(arrayObj); + value().setObject(*arrayObj); + + // |needsWrapping| isn't tracked here, because all modifications on the + // initial elements don't require any wrapping. + } + + void initialize(PromiseCombinatorDataHolder* data, ArrayObject* arrayObj, + bool needsWrapping) { + unwrappedArray().set(arrayObj); + value().set(data->valuesArray()); + elements().setElementNeedsWrapping = needsWrapping; + } + + [[nodiscard]] bool pushUndefined(JSContext* cx) { + // Helper for the AutoRealm we need to work with |array|. We mostly do this + // for performance; we could go ahead and do the define via a cross- + // compartment proxy instead... + AutoRealm ar(cx, unwrappedArray()); + + Handle<ArrayObject*> arrayObj = unwrappedArray(); + return js::NewbornArrayPush(cx, arrayObj, UndefinedValue()); + } + + // `Promise.all` Resolve Element Functions + // Step 9. Set values[index] to x. + // + // `Promise.allSettled` Resolve Element Functions + // `Promise.allSettled` Reject Element Functions + // Step 12. Set values[index] to obj. + // + // `Promise.any` Reject Element Functions + // Step 9. Set errors[index] to x. + // + // These handler functions are always created in the compartment of the + // Promise.all/allSettled/any function, which isn't necessarily the same + // compartment as unwrappedArray as explained in NewPromiseCombinatorElements. + // So before storing |val| we may need to enter unwrappedArray's compartment. + [[nodiscard]] bool setElement(JSContext* cx, uint32_t index, + HandleValue val) { + // The index is guaranteed to be initialized to `undefined`. + MOZ_ASSERT(unwrappedArray()->getDenseElement(index).isUndefined()); + + if (elements().setElementNeedsWrapping) { + AutoRealm ar(cx, unwrappedArray()); + + RootedValue rootedVal(cx, val); + if (!cx->compartment()->wrap(cx, &rootedVal)) { + return false; + } + unwrappedArray()->setDenseElement(index, rootedVal); + } else { + unwrappedArray()->setDenseElement(index, val); + } + return true; + } +}; + +} // namespace js + +PromiseCombinatorDataHolder* PromiseCombinatorDataHolder::New( + JSContext* cx, HandleObject resultPromise, + Handle<PromiseCombinatorElements> elements, HandleObject resolveOrReject) { + auto* dataHolder = NewBuiltinClassInstance<PromiseCombinatorDataHolder>(cx); + if (!dataHolder) { + return nullptr; + } + + cx->check(resultPromise); + cx->check(elements.value()); + cx->check(resolveOrReject); + + dataHolder->setFixedSlot(Slot_Promise, ObjectValue(*resultPromise)); + dataHolder->setFixedSlot(Slot_RemainingElements, Int32Value(1)); + dataHolder->setFixedSlot(Slot_ValuesArray, elements.value()); + dataHolder->setFixedSlot(Slot_ResolveOrRejectFunction, + ObjectValue(*resolveOrReject)); + return dataHolder; +} + +namespace { +// Generator used by PromiseObject::getID. +mozilla::Atomic<uint64_t> gIDGenerator(0); +} // namespace + +class PromiseDebugInfo : public NativeObject { + private: + enum Slots { + Slot_AllocationSite, + Slot_ResolutionSite, + Slot_AllocationTime, + Slot_ResolutionTime, + Slot_Id, + SlotCount + }; + + public: + static const JSClass class_; + static PromiseDebugInfo* create(JSContext* cx, + Handle<PromiseObject*> promise) { + Rooted<PromiseDebugInfo*> debugInfo( + cx, NewBuiltinClassInstance<PromiseDebugInfo>(cx)); + if (!debugInfo) { + return nullptr; + } + + RootedObject stack(cx); + if (!JS::CaptureCurrentStack(cx, &stack, + JS::StackCapture(JS::AllFrames()))) { + return nullptr; + } + debugInfo->setFixedSlot(Slot_AllocationSite, ObjectOrNullValue(stack)); + debugInfo->setFixedSlot(Slot_ResolutionSite, NullValue()); + debugInfo->setFixedSlot(Slot_AllocationTime, + DoubleValue(MillisecondsSinceStartup())); + debugInfo->setFixedSlot(Slot_ResolutionTime, NumberValue(0)); + promise->setFixedSlot(PromiseSlot_DebugInfo, ObjectValue(*debugInfo)); + + return debugInfo; + } + + static PromiseDebugInfo* FromPromise(PromiseObject* promise) { + Value val = promise->getFixedSlot(PromiseSlot_DebugInfo); + if (val.isObject()) { + return &val.toObject().as<PromiseDebugInfo>(); + } + return nullptr; + } + + /** + * Returns the given PromiseObject's process-unique ID. + * The ID is lazily assigned when first queried, and then either stored + * in the DebugInfo slot if no debug info was recorded for this Promise, + * or in the Id slot of the DebugInfo object. + */ + static uint64_t id(PromiseObject* promise) { + Value idVal(promise->getFixedSlot(PromiseSlot_DebugInfo)); + if (idVal.isUndefined()) { + idVal.setDouble(++gIDGenerator); + promise->setFixedSlot(PromiseSlot_DebugInfo, idVal); + } else if (idVal.isObject()) { + PromiseDebugInfo* debugInfo = FromPromise(promise); + idVal = debugInfo->getFixedSlot(Slot_Id); + if (idVal.isUndefined()) { + idVal.setDouble(++gIDGenerator); + debugInfo->setFixedSlot(Slot_Id, idVal); + } + } + return uint64_t(idVal.toNumber()); + } + + double allocationTime() { + return getFixedSlot(Slot_AllocationTime).toNumber(); + } + double resolutionTime() { + return getFixedSlot(Slot_ResolutionTime).toNumber(); + } + JSObject* allocationSite() { + return getFixedSlot(Slot_AllocationSite).toObjectOrNull(); + } + JSObject* resolutionSite() { + return getFixedSlot(Slot_ResolutionSite).toObjectOrNull(); + } + + // The |unwrappedRejectionStack| parameter should only be set on promise + // rejections and should be the stack of the exception that caused the promise + // to be rejected. If the |unwrappedRejectionStack| is null, the current stack + // will be used instead. This is also the default behavior for fulfilled + // promises. + static void setResolutionInfo(JSContext* cx, Handle<PromiseObject*> promise, + Handle<SavedFrame*> unwrappedRejectionStack) { + MOZ_ASSERT_IF(unwrappedRejectionStack, + promise->state() == JS::PromiseState::Rejected); + + if (!JS::IsAsyncStackCaptureEnabledForRealm(cx)) { + return; + } + + // If async stacks weren't enabled and the Promise's global wasn't a + // debuggee when the Promise was created, we won't have a debugInfo + // object. We still want to capture the resolution stack, so we + // create the object now and change it's slots' values around a bit. + Rooted<PromiseDebugInfo*> debugInfo(cx, FromPromise(promise)); + if (!debugInfo) { + RootedValue idVal(cx, promise->getFixedSlot(PromiseSlot_DebugInfo)); + debugInfo = create(cx, promise); + if (!debugInfo) { + cx->clearPendingException(); + return; + } + + // The current stack was stored in the AllocationSite slot, move + // it to ResolutionSite as that's what it really is. + debugInfo->setFixedSlot(Slot_ResolutionSite, + debugInfo->getFixedSlot(Slot_AllocationSite)); + debugInfo->setFixedSlot(Slot_AllocationSite, NullValue()); + + // There's no good default for a missing AllocationTime, so + // instead of resetting that, ensure that it's the same as + // ResolutionTime, so that the diff shows as 0, which isn't great, + // but bearable. + debugInfo->setFixedSlot(Slot_ResolutionTime, + debugInfo->getFixedSlot(Slot_AllocationTime)); + + // The Promise's ID might've been queried earlier, in which case + // it's stored in the DebugInfo slot. We saved that earlier, so + // now we can store it in the right place (or leave it as + // undefined if it wasn't ever initialized.) + debugInfo->setFixedSlot(Slot_Id, idVal); + return; + } + + RootedObject stack(cx, unwrappedRejectionStack); + if (stack) { + // The exception stack is always unwrapped so it might be in + // a different compartment. + if (!cx->compartment()->wrap(cx, &stack)) { + cx->clearPendingException(); + return; + } + } else { + if (!JS::CaptureCurrentStack(cx, &stack, + JS::StackCapture(JS::AllFrames()))) { + cx->clearPendingException(); + return; + } + } + + debugInfo->setFixedSlot(Slot_ResolutionSite, ObjectOrNullValue(stack)); + debugInfo->setFixedSlot(Slot_ResolutionTime, + DoubleValue(MillisecondsSinceStartup())); + } +}; + +const JSClass PromiseDebugInfo::class_ = { + "PromiseDebugInfo", JSCLASS_HAS_RESERVED_SLOTS(SlotCount)}; + +double PromiseObject::allocationTime() { + auto debugInfo = PromiseDebugInfo::FromPromise(this); + if (debugInfo) { + return debugInfo->allocationTime(); + } + return 0; +} + +double PromiseObject::resolutionTime() { + auto debugInfo = PromiseDebugInfo::FromPromise(this); + if (debugInfo) { + return debugInfo->resolutionTime(); + } + return 0; +} + +JSObject* PromiseObject::allocationSite() { + auto debugInfo = PromiseDebugInfo::FromPromise(this); + if (debugInfo) { + return debugInfo->allocationSite(); + } + return nullptr; +} + +JSObject* PromiseObject::resolutionSite() { + auto debugInfo = PromiseDebugInfo::FromPromise(this); + if (debugInfo) { + JSObject* site = debugInfo->resolutionSite(); + if (site && !JS_IsDeadWrapper(site)) { + MOZ_ASSERT(UncheckedUnwrap(site)->is<SavedFrame>()); + return site; + } + } + return nullptr; +} + +/** + * Wrapper for GetAndClearExceptionAndStack that handles cases where + * no exception is pending, but an error occurred. + * This can be the case if an OOM was encountered while throwing the error. + */ +static bool MaybeGetAndClearExceptionAndStack( + JSContext* cx, MutableHandleValue rval, MutableHandle<SavedFrame*> stack) { + if (!cx->isExceptionPending()) { + return false; + } + + return GetAndClearExceptionAndStack(cx, rval, stack); +} + +[[nodiscard]] static bool CallPromiseRejectFunction( + JSContext* cx, HandleObject rejectFun, HandleValue reason, + HandleObject promiseObj, Handle<SavedFrame*> unwrappedRejectionStack, + UnhandledRejectionBehavior behavior); + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * IfAbruptRejectPromise ( value, capability ) + * https://tc39.es/ecma262/#sec-ifabruptrejectpromise + * + * Steps 1.a-b. + * + * Extracting all of this internal spec algorithm into a helper function would + * be tedious, so the check in step 1 and the entirety of step 2 aren't + * included. + */ +static bool AbruptRejectPromise(JSContext* cx, CallArgs& args, + HandleObject promiseObj, HandleObject reject) { + // Step 1.a. Perform + // ? Call(capability.[[Reject]], undefined, « value.[[Value]] »). + RootedValue reason(cx); + Rooted<SavedFrame*> stack(cx); + if (!MaybeGetAndClearExceptionAndStack(cx, &reason, &stack)) { + return false; + } + + if (!CallPromiseRejectFunction(cx, reject, reason, promiseObj, stack, + UnhandledRejectionBehavior::Report)) { + return false; + } + + // Step 1.b. Return capability.[[Promise]]. + args.rval().setObject(*promiseObj); + return true; +} + +static bool AbruptRejectPromise(JSContext* cx, CallArgs& args, + Handle<PromiseCapability> capability) { + return AbruptRejectPromise(cx, args, capability.promise(), + capability.reject()); +} + +enum ReactionRecordSlots { + // This is the promise-like object that gets resolved with the result of this + // reaction, if any. If this reaction record was created with .then or .catch, + // this is the promise that .then or .catch returned. + // + // The spec says that a PromiseReaction record has a [[Capability]] field + // whose value is either undefined or a PromiseCapability record, but we just + // store the PromiseCapability's fields directly in this object. This is the + // capability's [[Promise]] field; its [[Resolve]] and [[Reject]] fields are + // stored in ReactionRecordSlot_Resolve and ReactionRecordSlot_Reject. + // + // This can be 'null' in reaction records created for a few situations: + // + // - When you resolve one promise to another. When you pass a promise P1 to + // the 'fulfill' function of a promise P2, so that resolving P1 resolves P2 + // in the same way, P1 gets a reaction record with the + // REACTION_FLAG_DEFAULT_RESOLVING_HANDLER flag set and whose + // ReactionRecordSlot_GeneratorOrPromiseToResolve slot holds P2. + // + // - When you await a promise. When an async function or generator awaits a + // value V, then the await expression generates an internal promise P, + // resolves it to V, and then gives P a reaction record with the + // REACTION_FLAG_ASYNC_FUNCTION or REACTION_FLAG_ASYNC_GENERATOR flag set + // and whose ReactionRecordSlot_GeneratorOrPromiseToResolve slot holds the + // generator object. (Typically V is a promise, so resolving P to V gives V + // a REACTION_FLAGS_DEFAULT_RESOLVING_HANDLER reaction record as described + // above.) + // + // - When JS::AddPromiseReactions{,IgnoringUnhandledRejection} cause the + // reaction to be created. (These functions act as if they had created a + // promise to invoke the appropriate provided reaction function, without + // actually allocating a promise for them.) + ReactionRecordSlot_Promise = 0, + + // The [[Handler]] field(s) of a PromiseReaction record. We create a + // single reaction record for fulfillment and rejection, therefore our + // PromiseReaction implementation needs two [[Handler]] fields. + // + // The slot value is either a callable object, an integer constant from + // the |PromiseHandler| enum, or null. If the value is null, either the + // REACTION_FLAG_DEBUGGER_DUMMY or the + // REACTION_FLAG_DEFAULT_RESOLVING_HANDLER flag must be set. + // + // After setting the target state for a PromiseReaction, the slot of the + // no longer used handler gets reused to store the argument of the active + // handler. + ReactionRecordSlot_OnFulfilled, + ReactionRecordSlot_OnRejectedArg = ReactionRecordSlot_OnFulfilled, + ReactionRecordSlot_OnRejected, + ReactionRecordSlot_OnFulfilledArg = ReactionRecordSlot_OnRejected, + + // The functions to resolve or reject the promise. Matches the + // [[Capability]].[[Resolve]] and [[Capability]].[[Reject]] fields from + // the spec. + // + // The slot values are either callable objects or null, but the latter + // case is only allowed if the promise is either a built-in Promise object + // or null. + ReactionRecordSlot_Resolve, + ReactionRecordSlot_Reject, + + // The incumbent global for this reaction record. Can be null. + ReactionRecordSlot_IncumbentGlobalObject, + + // Bitmask of the REACTION_FLAG values. + ReactionRecordSlot_Flags, + + // Additional slot to store extra data for specific reaction record types. + // + // - When the REACTION_FLAG_ASYNC_FUNCTION flag is set, this slot stores + // the (internal) generator object for this promise reaction. + // - When the REACTION_FLAG_ASYNC_GENERATOR flag is set, this slot stores + // the async generator object for this promise reaction. + // - When the REACTION_FLAG_DEFAULT_RESOLVING_HANDLER flag is set, this + // slot stores the promise to resolve when conceptually "calling" the + // OnFulfilled or OnRejected handlers. + ReactionRecordSlot_GeneratorOrPromiseToResolve, + + ReactionRecordSlots, +}; + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * PromiseReaction Records + * https://tc39.es/ecma262/#sec-promisereaction-records + */ +class PromiseReactionRecord : public NativeObject { + // If this flag is set, this reaction record is already enqueued to the + // job queue, and the spec's [[Type]] field is represented by + // REACTION_FLAG_FULFILLED flag. + // + // If this flag isn't yet set, [[Type]] field is undefined. + static constexpr uint32_t REACTION_FLAG_RESOLVED = 0x1; + + // This bit is valid only when REACTION_FLAG_RESOLVED flag is set. + // + // If this flag is set, [[Type]] field is Fulfill. + // If this flag isn't set, [[Type]] field is Reject. + static constexpr uint32_t REACTION_FLAG_FULFILLED = 0x2; + + // If this flag is set, this reaction record is created for resolving + // one promise P1 to another promise P2, and + // ReactionRecordSlot_GeneratorOrPromiseToResolve slot holds P2. + static constexpr uint32_t REACTION_FLAG_DEFAULT_RESOLVING_HANDLER = 0x4; + + // If this flag is set, this reaction record is created for async function + // and ReactionRecordSlot_GeneratorOrPromiseToResolve slot holds + // internal generator object of the async function. + static constexpr uint32_t REACTION_FLAG_ASYNC_FUNCTION = 0x8; + + // If this flag is set, this reaction record is created for async generator + // and ReactionRecordSlot_GeneratorOrPromiseToResolve slot holds + // the async generator object of the async generator. + static constexpr uint32_t REACTION_FLAG_ASYNC_GENERATOR = 0x10; + + // If this flag is set, this reaction record is created only for providing + // information to debugger. + static constexpr uint32_t REACTION_FLAG_DEBUGGER_DUMMY = 0x20; + + // This bit is valid only when the promise object is optimized out + // for the reaction. + // + // If this flag is set, unhandled rejection should be ignored. + // Otherwise, promise object should be created on-demand for unhandled + // rejection. + static constexpr uint32_t REACTION_FLAG_IGNORE_UNHANDLED_REJECTION = 0x40; + + void setFlagOnInitialState(uint32_t flag) { + int32_t flags = this->flags(); + MOZ_ASSERT(flags == 0, "Can't modify with non-default flags"); + flags |= flag; + setFixedSlot(ReactionRecordSlot_Flags, Int32Value(flags)); + } + + uint32_t handlerSlot() { + MOZ_ASSERT(targetState() != JS::PromiseState::Pending); + return targetState() == JS::PromiseState::Fulfilled + ? ReactionRecordSlot_OnFulfilled + : ReactionRecordSlot_OnRejected; + } + + uint32_t handlerArgSlot() { + MOZ_ASSERT(targetState() != JS::PromiseState::Pending); + return targetState() == JS::PromiseState::Fulfilled + ? ReactionRecordSlot_OnFulfilledArg + : ReactionRecordSlot_OnRejectedArg; + } + + public: + static const JSClass class_; + + JSObject* promise() { + return getFixedSlot(ReactionRecordSlot_Promise).toObjectOrNull(); + } + + int32_t flags() const { + return getFixedSlot(ReactionRecordSlot_Flags).toInt32(); + } + + JS::PromiseState targetState() { + int32_t flags = this->flags(); + if (!(flags & REACTION_FLAG_RESOLVED)) { + return JS::PromiseState::Pending; + } + return flags & REACTION_FLAG_FULFILLED ? JS::PromiseState::Fulfilled + : JS::PromiseState::Rejected; + } + void setTargetStateAndHandlerArg(JS::PromiseState state, const Value& arg) { + MOZ_ASSERT(targetState() == JS::PromiseState::Pending); + MOZ_ASSERT(state != JS::PromiseState::Pending, + "Can't revert a reaction to pending."); + + int32_t flags = this->flags(); + flags |= REACTION_FLAG_RESOLVED; + if (state == JS::PromiseState::Fulfilled) { + flags |= REACTION_FLAG_FULFILLED; + } + + setFixedSlot(ReactionRecordSlot_Flags, Int32Value(flags)); + setFixedSlot(handlerArgSlot(), arg); + } + + void setShouldIgnoreUnhandledRejection() { + setFlagOnInitialState(REACTION_FLAG_IGNORE_UNHANDLED_REJECTION); + } + UnhandledRejectionBehavior unhandledRejectionBehavior() const { + int32_t flags = this->flags(); + return (flags & REACTION_FLAG_IGNORE_UNHANDLED_REJECTION) + ? UnhandledRejectionBehavior::Ignore + : UnhandledRejectionBehavior::Report; + } + + void setIsDefaultResolvingHandler(PromiseObject* promiseToResolve) { + setFlagOnInitialState(REACTION_FLAG_DEFAULT_RESOLVING_HANDLER); + setFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve, + ObjectValue(*promiseToResolve)); + } + bool isDefaultResolvingHandler() { + int32_t flags = this->flags(); + return flags & REACTION_FLAG_DEFAULT_RESOLVING_HANDLER; + } + PromiseObject* defaultResolvingPromise() { + MOZ_ASSERT(isDefaultResolvingHandler()); + const Value& promiseToResolve = + getFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve); + return &promiseToResolve.toObject().as<PromiseObject>(); + } + + void setIsAsyncFunction(AsyncFunctionGeneratorObject* genObj) { + setFlagOnInitialState(REACTION_FLAG_ASYNC_FUNCTION); + setFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve, + ObjectValue(*genObj)); + } + bool isAsyncFunction() { + int32_t flags = this->flags(); + return flags & REACTION_FLAG_ASYNC_FUNCTION; + } + AsyncFunctionGeneratorObject* asyncFunctionGenerator() { + MOZ_ASSERT(isAsyncFunction()); + const Value& generator = + getFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve); + return &generator.toObject().as<AsyncFunctionGeneratorObject>(); + } + + void setIsAsyncGenerator(AsyncGeneratorObject* generator) { + setFlagOnInitialState(REACTION_FLAG_ASYNC_GENERATOR); + setFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve, + ObjectValue(*generator)); + } + bool isAsyncGenerator() { + int32_t flags = this->flags(); + return flags & REACTION_FLAG_ASYNC_GENERATOR; + } + AsyncGeneratorObject* asyncGenerator() { + MOZ_ASSERT(isAsyncGenerator()); + const Value& generator = + getFixedSlot(ReactionRecordSlot_GeneratorOrPromiseToResolve); + return &generator.toObject().as<AsyncGeneratorObject>(); + } + + void setIsDebuggerDummy() { + setFlagOnInitialState(REACTION_FLAG_DEBUGGER_DUMMY); + } + bool isDebuggerDummy() { + int32_t flags = this->flags(); + return flags & REACTION_FLAG_DEBUGGER_DUMMY; + } + + Value handler() { + MOZ_ASSERT(targetState() != JS::PromiseState::Pending); + return getFixedSlot(handlerSlot()); + } + Value handlerArg() { + MOZ_ASSERT(targetState() != JS::PromiseState::Pending); + return getFixedSlot(handlerArgSlot()); + } + + JSObject* getAndClearIncumbentGlobalObject() { + JSObject* obj = + getFixedSlot(ReactionRecordSlot_IncumbentGlobalObject).toObjectOrNull(); + setFixedSlot(ReactionRecordSlot_IncumbentGlobalObject, UndefinedValue()); + return obj; + } +}; + +const JSClass PromiseReactionRecord::class_ = { + "PromiseReactionRecord", JSCLASS_HAS_RESERVED_SLOTS(ReactionRecordSlots)}; + +static void AddPromiseFlags(PromiseObject& promise, int32_t flag) { + int32_t flags = promise.flags(); + promise.setFixedSlot(PromiseSlot_Flags, Int32Value(flags | flag)); +} + +static void RemovePromiseFlags(PromiseObject& promise, int32_t flag) { + int32_t flags = promise.flags(); + promise.setFixedSlot(PromiseSlot_Flags, Int32Value(flags & ~flag)); +} + +static bool PromiseHasAnyFlag(PromiseObject& promise, int32_t flag) { + return promise.flags() & flag; +} + +static bool ResolvePromiseFunction(JSContext* cx, unsigned argc, Value* vp); +static bool RejectPromiseFunction(JSContext* cx, unsigned argc, Value* vp); + +static JSFunction* GetResolveFunctionFromReject(JSFunction* reject); +static JSFunction* GetRejectFunctionFromResolve(JSFunction* resolve); + +#ifdef DEBUG + +/** + * Returns Promise Resolve Function's [[AlreadyResolved]].[[Value]]. + */ +static bool IsAlreadyResolvedMaybeWrappedResolveFunction( + JSObject* resolveFunObj) { + if (IsWrapper(resolveFunObj)) { + resolveFunObj = UncheckedUnwrap(resolveFunObj); + } + + JSFunction* resolveFun = &resolveFunObj->as<JSFunction>(); + MOZ_ASSERT(resolveFun->maybeNative() == ResolvePromiseFunction); + + bool alreadyResolved = + resolveFun->getExtendedSlot(ResolveFunctionSlot_Promise).isUndefined(); + + // Other slots should agree. + if (alreadyResolved) { + MOZ_ASSERT(resolveFun->getExtendedSlot(ResolveFunctionSlot_RejectFunction) + .isUndefined()); + } else { + JSFunction* rejectFun = GetRejectFunctionFromResolve(resolveFun); + MOZ_ASSERT( + !rejectFun->getExtendedSlot(RejectFunctionSlot_Promise).isUndefined()); + MOZ_ASSERT(!rejectFun->getExtendedSlot(RejectFunctionSlot_ResolveFunction) + .isUndefined()); + } + + return alreadyResolved; +} + +/** + * Returns Promise Reject Function's [[AlreadyResolved]].[[Value]]. + */ +static bool IsAlreadyResolvedMaybeWrappedRejectFunction( + JSObject* rejectFunObj) { + if (IsWrapper(rejectFunObj)) { + rejectFunObj = UncheckedUnwrap(rejectFunObj); + } + + JSFunction* rejectFun = &rejectFunObj->as<JSFunction>(); + MOZ_ASSERT(rejectFun->maybeNative() == RejectPromiseFunction); + + bool alreadyResolved = + rejectFun->getExtendedSlot(RejectFunctionSlot_Promise).isUndefined(); + + // Other slots should agree. + if (alreadyResolved) { + MOZ_ASSERT(rejectFun->getExtendedSlot(RejectFunctionSlot_ResolveFunction) + .isUndefined()); + } else { + JSFunction* resolveFun = GetResolveFunctionFromReject(rejectFun); + MOZ_ASSERT(!resolveFun->getExtendedSlot(ResolveFunctionSlot_Promise) + .isUndefined()); + MOZ_ASSERT(!resolveFun->getExtendedSlot(ResolveFunctionSlot_RejectFunction) + .isUndefined()); + } + + return alreadyResolved; +} + +#endif // DEBUG + +/** + * Set Promise Resolve Function's and Promise Reject Function's + * [[AlreadyResolved]].[[Value]] to true. + * + * `resolutionFun` can be either of them. + */ +static void SetAlreadyResolvedResolutionFunction(JSFunction* resolutionFun) { + JSFunction* resolve; + JSFunction* reject; + if (resolutionFun->maybeNative() == ResolvePromiseFunction) { + resolve = resolutionFun; + reject = GetRejectFunctionFromResolve(resolutionFun); + } else { + resolve = GetResolveFunctionFromReject(resolutionFun); + reject = resolutionFun; + } + + resolve->setExtendedSlot(ResolveFunctionSlot_Promise, UndefinedValue()); + resolve->setExtendedSlot(ResolveFunctionSlot_RejectFunction, + UndefinedValue()); + + reject->setExtendedSlot(RejectFunctionSlot_Promise, UndefinedValue()); + reject->setExtendedSlot(RejectFunctionSlot_ResolveFunction, UndefinedValue()); + + MOZ_ASSERT(IsAlreadyResolvedMaybeWrappedResolveFunction(resolve)); + MOZ_ASSERT(IsAlreadyResolvedMaybeWrappedRejectFunction(reject)); +} + +/** + * Returns true if given promise is created by + * CreatePromiseObjectWithoutResolutionFunctions. + */ +bool js::IsPromiseWithDefaultResolvingFunction(PromiseObject* promise) { + return PromiseHasAnyFlag(*promise, PROMISE_FLAG_DEFAULT_RESOLVING_FUNCTIONS); +} + +/** + * Returns Promise Resolve Function's [[AlreadyResolved]].[[Value]] for + * a promise created by CreatePromiseObjectWithoutResolutionFunctions. + */ +static bool IsAlreadyResolvedPromiseWithDefaultResolvingFunction( + PromiseObject* promise) { + MOZ_ASSERT(IsPromiseWithDefaultResolvingFunction(promise)); + + if (promise->as<PromiseObject>().state() != JS::PromiseState::Pending) { + MOZ_ASSERT(PromiseHasAnyFlag( + *promise, PROMISE_FLAG_DEFAULT_RESOLVING_FUNCTIONS_ALREADY_RESOLVED)); + return true; + } + + return PromiseHasAnyFlag( + *promise, PROMISE_FLAG_DEFAULT_RESOLVING_FUNCTIONS_ALREADY_RESOLVED); +} + +/** + * Set Promise Resolve Function's [[AlreadyResolved]].[[Value]] to true for + * a promise created by CreatePromiseObjectWithoutResolutionFunctions. + */ +void js::SetAlreadyResolvedPromiseWithDefaultResolvingFunction( + PromiseObject* promise) { + MOZ_ASSERT(IsPromiseWithDefaultResolvingFunction(promise)); + + promise->setFixedSlot( + PromiseSlot_Flags, + JS::Int32Value( + promise->flags() | + PROMISE_FLAG_DEFAULT_RESOLVING_FUNCTIONS_ALREADY_RESOLVED)); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * CreateResolvingFunctions ( promise ) + * https://tc39.es/ecma262/#sec-createresolvingfunctions + */ +[[nodiscard]] static MOZ_ALWAYS_INLINE bool CreateResolvingFunctions( + JSContext* cx, HandleObject promise, MutableHandleObject resolveFn, + MutableHandleObject rejectFn) { + // Step 1. Let alreadyResolved be the Record { [[Value]]: false }. + // (implicit, see steps 5-6, 10-11 below) + + // Step 2. Let stepsResolve be the algorithm steps defined in Promise Resolve + // Functions. + // Step 3. Let lengthResolve be the number of non-optional parameters of the + // function definition in Promise Resolve Functions. + // Step 4. Let resolve be + // ! CreateBuiltinFunction(stepsResolve, lengthResolve, "", + // « [[Promise]], [[AlreadyResolved]] »). + Handle<PropertyName*> funName = cx->names().empty; + resolveFn.set(NewNativeFunction(cx, ResolvePromiseFunction, 1, funName, + gc::AllocKind::FUNCTION_EXTENDED, + GenericObject)); + if (!resolveFn) { + return false; + } + + // Step 7. Let stepsReject be the algorithm steps defined in Promise Reject + // Functions. + // Step 8. Let lengthReject be the number of non-optional parameters of the + // function definition in Promise Reject Functions. + // Step 9. Let reject be + // ! CreateBuiltinFunction(stepsReject, lengthReject, "", + // « [[Promise]], [[AlreadyResolved]] »). + rejectFn.set(NewNativeFunction(cx, RejectPromiseFunction, 1, funName, + gc::AllocKind::FUNCTION_EXTENDED, + GenericObject)); + if (!rejectFn) { + return false; + } + + JSFunction* resolveFun = &resolveFn->as<JSFunction>(); + JSFunction* rejectFun = &rejectFn->as<JSFunction>(); + + // Step 5. Set resolve.[[Promise]] to promise. + // Step 6. Set resolve.[[AlreadyResolved]] to alreadyResolved. + // + // NOTE: We use these references as [[AlreadyResolved]].[[Value]]. + // See the comment in ResolveFunctionSlots for more details. + resolveFun->initExtendedSlot(ResolveFunctionSlot_Promise, + ObjectValue(*promise)); + resolveFun->initExtendedSlot(ResolveFunctionSlot_RejectFunction, + ObjectValue(*rejectFun)); + + // Step 10. Set reject.[[Promise]] to promise. + // Step 11. Set reject.[[AlreadyResolved]] to alreadyResolved. + // + // NOTE: We use these references as [[AlreadyResolved]].[[Value]]. + // See the comment in ResolveFunctionSlots for more details. + rejectFun->initExtendedSlot(RejectFunctionSlot_Promise, + ObjectValue(*promise)); + rejectFun->initExtendedSlot(RejectFunctionSlot_ResolveFunction, + ObjectValue(*resolveFun)); + + MOZ_ASSERT(!IsAlreadyResolvedMaybeWrappedResolveFunction(resolveFun)); + MOZ_ASSERT(!IsAlreadyResolvedMaybeWrappedRejectFunction(rejectFun)); + + // Step 12. Return the Record { [[Resolve]]: resolve, [[Reject]]: reject }. + return true; +} + +static bool IsSettledMaybeWrappedPromise(JSObject* promise) { + if (IsProxy(promise)) { + promise = UncheckedUnwrap(promise); + + // Caller needs to handle dead wrappers. + if (JS_IsDeadWrapper(promise)) { + return false; + } + } + + return promise->as<PromiseObject>().state() != JS::PromiseState::Pending; +} + +[[nodiscard]] static bool RejectMaybeWrappedPromise( + JSContext* cx, HandleObject promiseObj, HandleValue reason, + Handle<SavedFrame*> unwrappedRejectionStack); + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise Reject Functions + * https://tc39.es/ecma262/#sec-promise-reject-functions + */ +static bool RejectPromiseFunction(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JSFunction* reject = &args.callee().as<JSFunction>(); + HandleValue reasonVal = args.get(0); + + // Step 1. Let F be the active function object. + // Step 2. Assert: F has a [[Promise]] internal slot whose value is an Object. + // (implicit) + + // Step 3. Let promise be F.[[Promise]]. + const Value& promiseVal = reject->getExtendedSlot(RejectFunctionSlot_Promise); + + // Step 4. Let alreadyResolved be F.[[AlreadyResolved]]. + // Step 5. If alreadyResolved.[[Value]] is true, return undefined. + // + // If the Promise isn't available anymore, it has been resolved and the + // reference to it removed to make it eligible for collection. + bool alreadyResolved = promiseVal.isUndefined(); + MOZ_ASSERT(IsAlreadyResolvedMaybeWrappedRejectFunction(reject) == + alreadyResolved); + if (alreadyResolved) { + args.rval().setUndefined(); + return true; + } + + RootedObject promise(cx, &promiseVal.toObject()); + + // Step 6. Set alreadyResolved.[[Value]] to true. + SetAlreadyResolvedResolutionFunction(reject); + + // In some cases the Promise reference on the resolution function won't + // have been removed during resolution, so we need to check that here, + // too. + if (IsSettledMaybeWrappedPromise(promise)) { + args.rval().setUndefined(); + return true; + } + + // Step 7. Return RejectPromise(promise, reason). + if (!RejectMaybeWrappedPromise(cx, promise, reasonVal, nullptr)) { + return false; + } + args.rval().setUndefined(); + return true; +} + +[[nodiscard]] static bool FulfillMaybeWrappedPromise(JSContext* cx, + HandleObject promiseObj, + HandleValue value_); + +[[nodiscard]] static bool EnqueuePromiseResolveThenableJob( + JSContext* cx, HandleValue promiseToResolve, HandleValue thenable, + HandleValue thenVal); + +[[nodiscard]] static bool EnqueuePromiseResolveThenableBuiltinJob( + JSContext* cx, HandleObject promiseToResolve, HandleObject thenable); + +static bool Promise_then_impl(JSContext* cx, HandleValue promiseVal, + HandleValue onFulfilled, HandleValue onRejected, + MutableHandleValue rval, bool rvalExplicitlyUsed); + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise Resolve Functions + * https://tc39.es/ecma262/#sec-promise-resolve-functions + * + * Steps 7-15. + */ +[[nodiscard]] bool js::ResolvePromiseInternal( + JSContext* cx, JS::Handle<JSObject*> promise, + JS::Handle<JS::Value> resolutionVal) { + cx->check(promise, resolutionVal); + MOZ_ASSERT(!IsSettledMaybeWrappedPromise(promise)); + + // (reordered) + // Step 8. If Type(resolution) is not Object, then + if (!resolutionVal.isObject()) { + // Step 8.a. Return FulfillPromise(promise, resolution). + return FulfillMaybeWrappedPromise(cx, promise, resolutionVal); + } + + RootedObject resolution(cx, &resolutionVal.toObject()); + + // Step 7. If SameValue(resolution, promise) is true, then + if (resolution == promise) { + // Step 7.a. Let selfResolutionError be a newly created TypeError object. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_CANNOT_RESOLVE_PROMISE_WITH_ITSELF); + RootedValue selfResolutionError(cx); + Rooted<SavedFrame*> stack(cx); + if (!MaybeGetAndClearExceptionAndStack(cx, &selfResolutionError, &stack)) { + return false; + } + + // Step 7.b. Return RejectPromise(promise, selfResolutionError). + return RejectMaybeWrappedPromise(cx, promise, selfResolutionError, stack); + } + + // Step 9. Let then be Get(resolution, "then"). + RootedValue thenVal(cx); + bool status = + GetProperty(cx, resolution, resolution, cx->names().then, &thenVal); + + RootedValue error(cx); + Rooted<SavedFrame*> errorStack(cx); + + // Step 10. If then is an abrupt completion, then + if (!status) { + // Get the `then.[[Value]]` value used in the step 10.a. + if (!MaybeGetAndClearExceptionAndStack(cx, &error, &errorStack)) { + return false; + } + } + + // Testing functions allow to directly settle a promise without going + // through the resolving functions. In that case the normal bookkeeping to + // ensure only pending promises can be resolved doesn't apply and we need + // to manually check for already settled promises. The exception is simply + // dropped when this case happens. + if (IsSettledMaybeWrappedPromise(promise)) { + return true; + } + + // Step 10. If then is an abrupt completion, then + if (!status) { + // Step 10.a. Return RejectPromise(promise, then.[[Value]]). + return RejectMaybeWrappedPromise(cx, promise, error, errorStack); + } + + // Step 11. Let thenAction be then.[[Value]]. + // (implicit) + + // Step 12. If IsCallable(thenAction) is false, then + if (!IsCallable(thenVal)) { + // Step 12.a. Return FulfillPromise(promise, resolution). + return FulfillMaybeWrappedPromise(cx, promise, resolutionVal); + } + + // Step 13. Let thenJobCallback be HostMakeJobCallback(thenAction). + // (implicit) + + // Step 14. Let job be + // NewPromiseResolveThenableJob(promise, resolution, + // thenJobCallback). + // Step 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). + + // If the resolution object is a built-in Promise object and the + // `then` property is the original Promise.prototype.then function + // from the current realm, we skip storing/calling it. + // Additionally we require that |promise| itself is also a built-in + // Promise object, so the fast path doesn't need to cope with wrappers. + bool isBuiltinThen = false; + if (resolution->is<PromiseObject>() && promise->is<PromiseObject>() && + IsNativeFunction(thenVal, Promise_then) && + thenVal.toObject().as<JSFunction>().realm() == cx->realm()) { + isBuiltinThen = true; + } + + if (!isBuiltinThen) { + RootedValue promiseVal(cx, ObjectValue(*promise)); + if (!EnqueuePromiseResolveThenableJob(cx, promiseVal, resolutionVal, + thenVal)) { + return false; + } + } else { + if (!EnqueuePromiseResolveThenableBuiltinJob(cx, promise, resolution)) { + return false; + } + } + + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise Resolve Functions + * https://tc39.es/ecma262/#sec-promise-resolve-functions + */ +static bool ResolvePromiseFunction(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. Let F be the active function object. + // Step 2. Assert: F has a [[Promise]] internal slot whose value is an Object. + // (implicit) + + JSFunction* resolve = &args.callee().as<JSFunction>(); + HandleValue resolutionVal = args.get(0); + + // Step 3. Let promise be F.[[Promise]]. + const Value& promiseVal = + resolve->getExtendedSlot(ResolveFunctionSlot_Promise); + + // Step 4. Let alreadyResolved be F.[[AlreadyResolved]]. + // Step 5. If alreadyResolved.[[Value]] is true, return undefined. + // + // NOTE: We use the reference to the reject function as [[AlreadyResolved]]. + bool alreadyResolved = promiseVal.isUndefined(); + MOZ_ASSERT(IsAlreadyResolvedMaybeWrappedResolveFunction(resolve) == + alreadyResolved); + if (alreadyResolved) { + args.rval().setUndefined(); + return true; + } + + RootedObject promise(cx, &promiseVal.toObject()); + + // Step 6. Set alreadyResolved.[[Value]] to true. + SetAlreadyResolvedResolutionFunction(resolve); + + // In some cases the Promise reference on the resolution function won't + // have been removed during resolution, so we need to check that here, + // too. + if (IsSettledMaybeWrappedPromise(promise)) { + args.rval().setUndefined(); + return true; + } + + // Steps 7-15. + if (!ResolvePromiseInternal(cx, promise, resolutionVal)) { + return false; + } + + // Step 16. Return undefined. + args.rval().setUndefined(); + return true; +} + +static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp); + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * NewPromiseReactionJob ( reaction, argument ) + * https://tc39.es/ecma262/#sec-newpromisereactionjob + * HostEnqueuePromiseJob ( job, realm ) + * https://tc39.es/ecma262/#sec-hostenqueuepromisejob + * + * Tells the embedding to enqueue a Promise reaction job, based on + * three parameters: + * reactionObj - The reaction record. + * handlerArg_ - The first and only argument to pass to the handler invoked by + * the job. This will be stored on the reaction record. + * targetState - The PromiseState this reaction job targets. This decides + * whether the onFulfilled or onRejected handler is called. + */ +[[nodiscard]] static bool EnqueuePromiseReactionJob( + JSContext* cx, HandleObject reactionObj, HandleValue handlerArg_, + JS::PromiseState targetState) { + MOZ_ASSERT(targetState == JS::PromiseState::Fulfilled || + targetState == JS::PromiseState::Rejected); + + // The reaction might have been stored on a Promise from another + // compartment, which means it would've been wrapped in a CCW. + // To properly handle that case here, unwrap it and enter its + // compartment, where the job creation should take place anyway. + Rooted<PromiseReactionRecord*> reaction(cx); + RootedValue handlerArg(cx, handlerArg_); + mozilla::Maybe<AutoRealm> ar; + if (!IsProxy(reactionObj)) { + MOZ_RELEASE_ASSERT(reactionObj->is<PromiseReactionRecord>()); + reaction = &reactionObj->as<PromiseReactionRecord>(); + if (cx->realm() != reaction->realm()) { + // If the compartment has multiple realms, create the job in the + // reaction's realm. This is consistent with the code in the else-branch + // and avoids problems with running jobs against a dying global (Gecko + // drops such jobs). + ar.emplace(cx, reaction); + } + } else { + JSObject* unwrappedReactionObj = UncheckedUnwrap(reactionObj); + if (JS_IsDeadWrapper(unwrappedReactionObj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEAD_OBJECT); + return false; + } + reaction = &unwrappedReactionObj->as<PromiseReactionRecord>(); + MOZ_RELEASE_ASSERT(reaction->is<PromiseReactionRecord>()); + ar.emplace(cx, reaction); + if (!cx->compartment()->wrap(cx, &handlerArg)) { + return false; + } + } + + // Must not enqueue a reaction job more than once. + MOZ_ASSERT(reaction->targetState() == JS::PromiseState::Pending); + + // NOTE: Instead of capturing reaction and arguments separately in the + // Job Abstract Closure below, store arguments (= handlerArg) in + // reaction object and capture it. + // Also, set reaction.[[Type]] is represented by targetState here. + cx->check(handlerArg); + reaction->setTargetStateAndHandlerArg(targetState, handlerArg); + + RootedValue reactionVal(cx, ObjectValue(*reaction)); + RootedValue handler(cx, reaction->handler()); + + // NewPromiseReactionJob + // Step 2. Let handlerRealm be null. + // NOTE: Instead of passing job and realm separately, we use the job's + // JSFunction object's realm as the job's realm. + // So we should enter the handlerRealm before creating the job function. + // + // GetFunctionRealm performed inside AutoFunctionOrCurrentRealm uses checked + // unwrap and it can hit permission error if there's a security wrapper, and + // in that case the reaction job is created in the current realm, instead of + // the target function's realm. + // + // If this reaction crosses chrome/content boundary, and the security + // wrapper would allow "call" operation, it still works inside the + // reaction job. + // + // This behavior is observable only when the job belonging to the content + // realm stops working (*1, *2), and it won't matter in practice. + // + // *1: "we can run script" performed inside HostEnqueuePromiseJob + // in HTML spec + // https://html.spec.whatwg.org/#hostenqueuepromisejob + // https://html.spec.whatwg.org/#check-if-we-can-run-script + // https://html.spec.whatwg.org/#fully-active + // *2: nsIGlobalObject::IsDying performed inside PromiseJobRunnable::Run + // in our implementation + mozilla::Maybe<AutoFunctionOrCurrentRealm> ar2; + + // NewPromiseReactionJob + // Step 3. If reaction.[[Handler]] is not empty, then + if (handler.isObject()) { + // Step 3.a. Let getHandlerRealmResult be + // GetFunctionRealm(reaction.[[Handler]].[[Callback]]). + // Step 3.b. If getHandlerRealmResult is a normal completion, + // set handlerRealm to getHandlerRealmResult.[[Value]]. + // Step 3.c. Else, set handlerRealm to the current Realm Record. + // Step 3.d. NOTE: handlerRealm is never null unless the handler is + // undefined. When the handler is a revoked Proxy and no + // ECMAScript code runs, handlerRealm is used to create error + // objects. + RootedObject handlerObj(cx, &handler.toObject()); + ar2.emplace(cx, handlerObj); + + // We need to wrap the reaction to store it on the job function. + if (!cx->compartment()->wrap(cx, &reactionVal)) { + return false; + } + } + + // NewPromiseReactionJob + // Step 1. Let job be a new Job Abstract Closure with no parameters that + // captures reaction and argument and performs the following steps + // when called: + Handle<PropertyName*> funName = cx->names().empty; + RootedFunction job( + cx, NewNativeFunction(cx, PromiseReactionJob, 0, funName, + gc::AllocKind::FUNCTION_EXTENDED, GenericObject)); + if (!job) { + return false; + } + + job->setExtendedSlot(ReactionJobSlot_ReactionRecord, reactionVal); + + // When using JS::AddPromiseReactions{,IgnoringUnHandledRejection}, no actual + // promise is created, so we might not have one here. + // Additionally, we might have an object here that isn't an instance of + // Promise. This can happen if content overrides the value of + // Promise[@@species] (or invokes Promise#then on a Promise subclass + // instance with a non-default @@species value on the constructor) with a + // function that returns objects that're not Promise (subclass) instances. + // In that case, we just pretend we didn't have an object in the first + // place. + // If after all this we do have an object, wrap it in case we entered the + // handler's compartment above, because we should pass objects from a + // single compartment to the enqueuePromiseJob callback. + RootedObject promise(cx, reaction->promise()); + if (promise) { + if (promise->is<PromiseObject>()) { + if (!cx->compartment()->wrap(cx, &promise)) { + return false; + } + } else if (IsWrapper(promise)) { + // `promise` can be already-wrapped promise object at this point. + JSObject* unwrappedPromise = UncheckedUnwrap(promise); + if (unwrappedPromise->is<PromiseObject>()) { + if (!cx->compartment()->wrap(cx, &promise)) { + return false; + } + } else { + promise = nullptr; + } + } else { + promise = nullptr; + } + } + + // Using objectFromIncumbentGlobal, we can derive the incumbent global by + // unwrapping and then getting the global. This is very convoluted, but + // much better than having to store the original global as a private value + // because we couldn't wrap it to store it as a normal JS value. + Rooted<GlobalObject*> global(cx); + if (JSObject* objectFromIncumbentGlobal = + reaction->getAndClearIncumbentGlobalObject()) { + objectFromIncumbentGlobal = CheckedUnwrapStatic(objectFromIncumbentGlobal); + MOZ_ASSERT(objectFromIncumbentGlobal); + global = &objectFromIncumbentGlobal->nonCCWGlobal(); + } + + // HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). + // + // Note: the global we pass here might be from a different compartment + // than job and promise. While it's somewhat unusual to pass objects + // from multiple compartments, in this case we specifically need the + // global to be unwrapped because wrapping and unwrapping aren't + // necessarily symmetric for globals. + return cx->runtime()->enqueuePromiseJob(cx, job, promise, global); +} + +[[nodiscard]] static bool TriggerPromiseReactions(JSContext* cx, + HandleValue reactionsVal, + JS::PromiseState state, + HandleValue valueOrReason); + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * FulfillPromise ( promise, value ) + * https://tc39.es/ecma262/#sec-fulfillpromise + * RejectPromise ( promise, reason ) + * https://tc39.es/ecma262/#sec-rejectpromise + * + * This method takes an additional optional |unwrappedRejectionStack| parameter, + * which is only used for debugging purposes. + * It allows callers to to pass in the stack of some exception which + * triggered the rejection of the promise. + */ +[[nodiscard]] static bool ResolvePromise( + JSContext* cx, Handle<PromiseObject*> promise, HandleValue valueOrReason, + JS::PromiseState state, + Handle<SavedFrame*> unwrappedRejectionStack = nullptr) { + // Step 1. Assert: The value of promise.[[PromiseState]] is pending. + MOZ_ASSERT(promise->state() == JS::PromiseState::Pending); + MOZ_ASSERT(state == JS::PromiseState::Fulfilled || + state == JS::PromiseState::Rejected); + MOZ_ASSERT_IF(unwrappedRejectionStack, state == JS::PromiseState::Rejected); + + // FulfillPromise + // Step 2. Let reactions be promise.[[PromiseFulfillReactions]]. + // RejectPromise + // Step 2. Let reactions be promise.[[PromiseRejectReactions]]. + // + // We only have one list of reactions for both resolution types. So + // instead of getting the right list of reactions, we determine the + // resolution type to retrieve the right information from the + // reaction records. + RootedValue reactionsVal(cx, promise->reactions()); + + // FulfillPromise + // Step 3. Set promise.[[PromiseResult]] to value. + // RejectPromise + // Step 3. Set promise.[[PromiseResult]] to reason. + // + // Step 4. Set promise.[[PromiseFulfillReactions]] to undefined. + // Step 5. Set promise.[[PromiseRejectReactions]] to undefined. + // + // The same slot is used for the reactions list and the result, so setting + // the result also removes the reactions list. + promise->setFixedSlot(PromiseSlot_ReactionsOrResult, valueOrReason); + + // FulfillPromise + // Step 6. Set promise.[[PromiseState]] to fulfilled. + // RejectPromise + // Step 6. Set promise.[[PromiseState]] to rejected. + int32_t flags = promise->flags(); + flags |= PROMISE_FLAG_RESOLVED; + if (state == JS::PromiseState::Fulfilled) { + flags |= PROMISE_FLAG_FULFILLED; + } + promise->setFixedSlot(PromiseSlot_Flags, Int32Value(flags)); + + // Also null out the resolve/reject functions so they can be GC'd. + promise->setFixedSlot(PromiseSlot_RejectFunction, UndefinedValue()); + + // Now that everything else is done, do the things the debugger needs. + + // RejectPromise + // Step 7. If promise.[[PromiseIsHandled]] is false, perform + // HostPromiseRejectionTracker(promise, "reject"). + PromiseObject::onSettled(cx, promise, unwrappedRejectionStack); + + // FulfillPromise + // Step 7. Return TriggerPromiseReactions(reactions, value). + // RejectPromise + // Step 8. Return TriggerPromiseReactions(reactions, reason). + return TriggerPromiseReactions(cx, reactionsVal, state, valueOrReason); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * RejectPromise ( promise, reason ) + * https://tc39.es/ecma262/#sec-rejectpromise + */ +[[nodiscard]] bool js::RejectPromiseInternal( + JSContext* cx, JS::Handle<PromiseObject*> promise, + JS::Handle<JS::Value> reason, + JS::Handle<SavedFrame*> unwrappedRejectionStack /* = nullptr */) { + return ResolvePromise(cx, promise, reason, JS::PromiseState::Rejected, + unwrappedRejectionStack); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * FulfillPromise ( promise, value ) + * https://tc39.es/ecma262/#sec-fulfillpromise + */ +[[nodiscard]] static bool FulfillMaybeWrappedPromise(JSContext* cx, + HandleObject promiseObj, + HandleValue value_) { + Rooted<PromiseObject*> promise(cx); + RootedValue value(cx, value_); + + mozilla::Maybe<AutoRealm> ar; + if (!IsProxy(promiseObj)) { + promise = &promiseObj->as<PromiseObject>(); + } else { + JSObject* unwrappedPromiseObj = UncheckedUnwrap(promiseObj); + if (JS_IsDeadWrapper(unwrappedPromiseObj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEAD_OBJECT); + return false; + } + promise = &unwrappedPromiseObj->as<PromiseObject>(); + ar.emplace(cx, promise); + if (!cx->compartment()->wrap(cx, &value)) { + return false; + } + } + + return ResolvePromise(cx, promise, value, JS::PromiseState::Fulfilled); +} + +static bool GetCapabilitiesExecutor(JSContext* cx, unsigned argc, Value* vp); +static bool PromiseConstructor(JSContext* cx, unsigned argc, Value* vp); +[[nodiscard]] static PromiseObject* CreatePromiseObjectInternal( + JSContext* cx, HandleObject proto = nullptr, bool protoIsWrapped = false, + bool informDebugger = true); + +enum GetCapabilitiesExecutorSlots { + GetCapabilitiesExecutorSlots_Resolve, + GetCapabilitiesExecutorSlots_Reject +}; + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise ( executor ) + * https://tc39.es/ecma262/#sec-promise-executor + */ +[[nodiscard]] static PromiseObject* +CreatePromiseObjectWithoutResolutionFunctions(JSContext* cx) { + // Steps 3-7. + PromiseObject* promise = CreatePromiseObjectInternal(cx); + if (!promise) { + return nullptr; + } + + AddPromiseFlags(*promise, PROMISE_FLAG_DEFAULT_RESOLVING_FUNCTIONS); + + // Step 11. Return promise. + return promise; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise ( executor ) + * https://tc39.es/ecma262/#sec-promise-executor + * + * As if called with GetCapabilitiesExecutor as the executor argument. + */ +[[nodiscard]] static PromiseObject* CreatePromiseWithDefaultResolutionFunctions( + JSContext* cx, MutableHandleObject resolve, MutableHandleObject reject) { + // Steps 3-7. + Rooted<PromiseObject*> promise(cx, CreatePromiseObjectInternal(cx)); + if (!promise) { + return nullptr; + } + + // Step 8. Let resolvingFunctions be CreateResolvingFunctions(promise). + if (!CreateResolvingFunctions(cx, promise, resolve, reject)) { + return nullptr; + } + + promise->setFixedSlot(PromiseSlot_RejectFunction, ObjectValue(*reject)); + + // Step 11. Return promise. + return promise; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * NewPromiseCapability ( C ) + * https://tc39.es/ecma262/#sec-newpromisecapability + */ +[[nodiscard]] static bool NewPromiseCapability( + JSContext* cx, HandleObject C, MutableHandle<PromiseCapability> capability, + bool canOmitResolutionFunctions) { + RootedValue cVal(cx, ObjectValue(*C)); + + // Step 1. If IsConstructor(C) is false, throw a TypeError exception. + // Step 2. NOTE: C is assumed to be a constructor function that supports the + // parameter conventions of the Promise constructor (see 27.2.3.1). + if (!IsConstructor(C)) { + ReportValueError(cx, JSMSG_NOT_CONSTRUCTOR, JSDVG_SEARCH_STACK, cVal, + nullptr); + return false; + } + + // If we'd call the original Promise constructor and know that the + // resolve/reject functions won't ever escape to content, we can skip + // creating and calling the executor function and instead return a Promise + // marked as having default resolve/reject functions. + // + // This can't be used in Promise.all and Promise.race because we have to + // pass the reject (and resolve, in the race case) function to thenables + // in the list passed to all/race, which (potentially) means exposing them + // to content. + // + // For Promise.all and Promise.race we can only optimize away the creation + // of the GetCapabilitiesExecutor function, and directly allocate the + // result promise instead of invoking the Promise constructor. + if (IsNativeFunction(cVal, PromiseConstructor) && + cVal.toObject().nonCCWRealm() == cx->realm()) { + PromiseObject* promise; + if (canOmitResolutionFunctions) { + promise = CreatePromiseObjectWithoutResolutionFunctions(cx); + } else { + promise = CreatePromiseWithDefaultResolutionFunctions( + cx, capability.resolve(), capability.reject()); + } + if (!promise) { + return false; + } + + // Step 3. Let promiseCapability be the PromiseCapability Record + // { [[Promise]]: undefined, [[Resolve]]: undefined, + // [[Reject]]: undefined }. + capability.promise().set(promise); + + // Step 10. Return promiseCapability. + return true; + } + + // Step 4. Let executorClosure be a new Abstract Closure with parameters + // (resolve, reject) that captures promiseCapability and performs the + // following steps when called: + Handle<PropertyName*> funName = cx->names().empty; + RootedFunction executor( + cx, NewNativeFunction(cx, GetCapabilitiesExecutor, 2, funName, + gc::AllocKind::FUNCTION_EXTENDED, GenericObject)); + if (!executor) { + return false; + } + + // Step 5. Let executor be + // ! CreateBuiltinFunction(executorClosure, 2, "", « »). + // (omitted) + + // Step 6. Let promise be ? Construct(C, « executor »). + // Step 9. Set promiseCapability.[[Promise]] to promise. + FixedConstructArgs<1> cargs(cx); + cargs[0].setObject(*executor); + if (!Construct(cx, cVal, cargs, cVal, capability.promise())) { + return false; + } + + // Step 7. If IsCallable(promiseCapability.[[Resolve]]) is false, + // throw a TypeError exception. + const Value& resolveVal = + executor->getExtendedSlot(GetCapabilitiesExecutorSlots_Resolve); + if (!IsCallable(resolveVal)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_PROMISE_RESOLVE_FUNCTION_NOT_CALLABLE); + return false; + } + + // Step 8. If IsCallable(promiseCapability.[[Reject]]) is false, + // throw a TypeError exception. + const Value& rejectVal = + executor->getExtendedSlot(GetCapabilitiesExecutorSlots_Reject); + if (!IsCallable(rejectVal)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_PROMISE_REJECT_FUNCTION_NOT_CALLABLE); + return false; + } + + // (reordered) + // Step 3. Let promiseCapability be the PromiseCapability Record + // { [[Promise]]: undefined, [[Resolve]]: undefined, + // [[Reject]]: undefined }. + capability.resolve().set(&resolveVal.toObject()); + capability.reject().set(&rejectVal.toObject()); + + // Step 10. Return promiseCapability. + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * NewPromiseCapability ( C ) + * https://tc39.es/ecma262/#sec-newpromisecapability + * + * Steps 4.a-e. + */ +static bool GetCapabilitiesExecutor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + JSFunction* F = &args.callee().as<JSFunction>(); + + // Step 4.a. If promiseCapability.[[Resolve]] is not undefined, + // throw a TypeError exception. + // Step 4.b. If promiseCapability.[[Reject]] is not undefined, + // throw a TypeError exception. + if (!F->getExtendedSlot(GetCapabilitiesExecutorSlots_Resolve).isUndefined() || + !F->getExtendedSlot(GetCapabilitiesExecutorSlots_Reject).isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_PROMISE_CAPABILITY_HAS_SOMETHING_ALREADY); + return false; + } + + // Step 4.c. Set promiseCapability.[[Resolve]] to resolve. + F->setExtendedSlot(GetCapabilitiesExecutorSlots_Resolve, args.get(0)); + + // Step 4.d. Set promiseCapability.[[Reject]] to reject. + F->setExtendedSlot(GetCapabilitiesExecutorSlots_Reject, args.get(1)); + + // Step 4.e. Return undefined. + args.rval().setUndefined(); + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * RejectPromise ( promise, reason ) + * https://tc39.es/ecma262/#sec-rejectpromise + */ +[[nodiscard]] static bool RejectMaybeWrappedPromise( + JSContext* cx, HandleObject promiseObj, HandleValue reason_, + Handle<SavedFrame*> unwrappedRejectionStack) { + Rooted<PromiseObject*> promise(cx); + RootedValue reason(cx, reason_); + + mozilla::Maybe<AutoRealm> ar; + if (!IsProxy(promiseObj)) { + promise = &promiseObj->as<PromiseObject>(); + } else { + JSObject* unwrappedPromiseObj = UncheckedUnwrap(promiseObj); + if (JS_IsDeadWrapper(unwrappedPromiseObj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEAD_OBJECT); + return false; + } + promise = &unwrappedPromiseObj->as<PromiseObject>(); + ar.emplace(cx, promise); + + // The rejection reason might've been created in a compartment with higher + // privileges than the Promise's. In that case, object-type rejection + // values might be wrapped into a wrapper that throws whenever the + // Promise's reaction handler wants to do anything useful with it. To + // avoid that situation, we synthesize a generic error that doesn't + // expose any privileged information but can safely be used in the + // rejection handler. + if (!cx->compartment()->wrap(cx, &reason)) { + return false; + } + if (reason.isObject() && !CheckedUnwrapStatic(&reason.toObject())) { + // Report the existing reason, so we don't just drop it on the + // floor. + JSObject* realReason = UncheckedUnwrap(&reason.toObject()); + RootedValue realReasonVal(cx, ObjectValue(*realReason)); + Rooted<GlobalObject*> realGlobal(cx, &realReason->nonCCWGlobal()); + ReportErrorToGlobal(cx, realGlobal, realReasonVal); + + // Async stacks are only properly adopted if there's at least one + // interpreter frame active right now. If a thenable job with a + // throwing `then` function got us here, that'll not be the case, + // so we add one by throwing the error from self-hosted code. + if (!GetInternalError(cx, JSMSG_PROMISE_ERROR_IN_WRAPPED_REJECTION_REASON, + &reason)) { + return false; + } + } + } + + return ResolvePromise(cx, promise, reason, JS::PromiseState::Rejected, + unwrappedRejectionStack); +} + +// Apply f to a mutable handle on each member of a collection of reactions, like +// that stored in PromiseSlot_ReactionsOrResult on a pending promise. When the +// reaction record is wrapped, we pass the wrapper, without dereferencing it. If +// f returns false, then we stop the iteration immediately and return false. +// Otherwise, we return true. +// +// There are several different representations for collections: +// +// - We represent an empty collection of reactions as an 'undefined' value. +// +// - We represent a collection containing a single reaction simply as the given +// PromiseReactionRecord object, possibly wrapped. +// +// - We represent a collection of two or more reactions as a dense array of +// possibly-wrapped PromiseReactionRecords. +// +template <typename F> +static bool ForEachReaction(JSContext* cx, HandleValue reactionsVal, F f) { + if (reactionsVal.isUndefined()) { + return true; + } + + RootedObject reactions(cx, &reactionsVal.toObject()); + RootedObject reaction(cx); + + if (reactions->is<PromiseReactionRecord>() || IsWrapper(reactions) || + JS_IsDeadWrapper(reactions)) { + return f(&reactions); + } + + Handle<NativeObject*> reactionsList = reactions.as<NativeObject>(); + uint32_t reactionsCount = reactionsList->getDenseInitializedLength(); + MOZ_ASSERT(reactionsCount > 1, "Reactions list should be created lazily"); + + for (uint32_t i = 0; i < reactionsCount; i++) { + const Value& reactionVal = reactionsList->getDenseElement(i); + MOZ_RELEASE_ASSERT(reactionVal.isObject()); + reaction = &reactionVal.toObject(); + if (!f(&reaction)) { + return false; + } + } + + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * TriggerPromiseReactions ( reactions, argument ) + * https://tc39.es/ecma262/#sec-triggerpromisereactions + */ +[[nodiscard]] static bool TriggerPromiseReactions(JSContext* cx, + HandleValue reactionsVal, + JS::PromiseState state, + HandleValue valueOrReason) { + MOZ_ASSERT(state == JS::PromiseState::Fulfilled || + state == JS::PromiseState::Rejected); + + // Step 1. For each element reaction of reactions, do + // Step 2. Return undefined. + return ForEachReaction(cx, reactionsVal, [&](MutableHandleObject reaction) { + // Step 1.a. Let job be NewPromiseReactionJob(reaction, argument). + // Step 1.b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). + return EnqueuePromiseReactionJob(cx, reaction, valueOrReason, state); + }); +} + +[[nodiscard]] static bool CallPromiseResolveFunction(JSContext* cx, + HandleObject resolveFun, + HandleValue value, + HandleObject promiseObj); + +/** + * ES2023 draft rev 714fa3dd1e8237ae9c666146270f81880089eca5 + * + * NewPromiseReactionJob ( reaction, argument ) + * https://tc39.es/ecma262/#sec-newpromisereactionjob + * + * Step 1. + * + * Implements PromiseReactionJob optimized for the case when the reaction + * handler is one of the default resolving functions as created by the + * CreateResolvingFunctions abstract operation. + */ +[[nodiscard]] static bool DefaultResolvingPromiseReactionJob( + JSContext* cx, Handle<PromiseReactionRecord*> reaction) { + MOZ_ASSERT(reaction->targetState() != JS::PromiseState::Pending); + + Rooted<PromiseObject*> promiseToResolve(cx, + reaction->defaultResolvingPromise()); + + // Testing functions allow to directly settle a promise without going + // through the resolving functions. In that case the normal bookkeeping to + // ensure only pending promises can be resolved doesn't apply and we need + // to manually check for already settled promises. We still call + // Run{Fulfill,Reject}Function for consistency with PromiseReactionJob. + ResolutionMode resolutionMode = ResolveMode; + RootedValue handlerResult(cx, UndefinedValue()); + Rooted<SavedFrame*> unwrappedRejectionStack(cx); + if (promiseToResolve->state() == JS::PromiseState::Pending) { + RootedValue argument(cx, reaction->handlerArg()); + + // Step 1.e. Else, let handlerResult be + // Completion(HostCallJobCallback(handler, undefined, + // « argument »)). + bool ok; + if (reaction->targetState() == JS::PromiseState::Fulfilled) { + ok = ResolvePromiseInternal(cx, promiseToResolve, argument); + } else { + ok = RejectPromiseInternal(cx, promiseToResolve, argument); + } + + if (!ok) { + resolutionMode = RejectMode; + if (!MaybeGetAndClearExceptionAndStack(cx, &handlerResult, + &unwrappedRejectionStack)) { + return false; + } + } + } + + // Steps 1.f-i. + RootedObject promiseObj(cx, reaction->promise()); + RootedObject callee(cx); + if (resolutionMode == ResolveMode) { + callee = + reaction->getFixedSlot(ReactionRecordSlot_Resolve).toObjectOrNull(); + + return CallPromiseResolveFunction(cx, callee, handlerResult, promiseObj); + } + + callee = reaction->getFixedSlot(ReactionRecordSlot_Reject).toObjectOrNull(); + + return CallPromiseRejectFunction(cx, callee, handlerResult, promiseObj, + unwrappedRejectionStack, + reaction->unhandledRejectionBehavior()); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Await in async function + * https://tc39.es/ecma262/#await + * + * Step 3. fulfilledClosure Abstract Closure. + * Step 5. rejectedClosure Abstract Closure. + */ +[[nodiscard]] static bool AsyncFunctionPromiseReactionJob( + JSContext* cx, Handle<PromiseReactionRecord*> reaction) { + MOZ_ASSERT(reaction->isAsyncFunction()); + + auto handler = static_cast<PromiseHandler>(reaction->handler().toInt32()); + RootedValue argument(cx, reaction->handlerArg()); + Rooted<AsyncFunctionGeneratorObject*> generator( + cx, reaction->asyncFunctionGenerator()); + + // Await's handlers don't return a value, nor throw any exceptions. + // They fail only on OOM. + + if (handler == PromiseHandler::AsyncFunctionAwaitedFulfilled) { + // Step 3. fulfilledClosure Abstract Closure. + return AsyncFunctionAwaitedFulfilled(cx, generator, argument); + } + + // Step 5. rejectedClosure Abstract Closure. + MOZ_ASSERT(handler == PromiseHandler::AsyncFunctionAwaitedRejected); + return AsyncFunctionAwaitedRejected(cx, generator, argument); +} + +/** + * ES2023 draft rev 714fa3dd1e8237ae9c666146270f81880089eca5 + * + * NewPromiseReactionJob ( reaction, argument ) + * https://tc39.es/ecma262/#sec-newpromisereactionjob + * + * Step 1. + * + * Callback triggering the fulfill/reject reaction for a resolved Promise, + * to be invoked by the embedding during its processing of the Promise job + * queue. + * + * A PromiseReactionJob is set as the native function of an extended + * JSFunction object, with all information required for the job's + * execution stored in in a reaction record in its first extended slot. + */ +static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedFunction job(cx, &args.callee().as<JSFunction>()); + + // Promise reactions don't return any value. + args.rval().setUndefined(); + + RootedObject reactionObj( + cx, &job->getExtendedSlot(ReactionJobSlot_ReactionRecord).toObject()); + + // To ensure that the embedding ends up with the right entry global, we're + // guaranteeing that the reaction job function gets created in the same + // compartment as the handler function. That's not necessarily the global + // that the job was triggered from, though. + // We can find the triggering global via the job's reaction record. To go + // back, we check if the reaction is a wrapper and if so, unwrap it and + // enter its compartment. + mozilla::Maybe<AutoRealm> ar; + if (!IsProxy(reactionObj)) { + MOZ_RELEASE_ASSERT(reactionObj->is<PromiseReactionRecord>()); + } else { + reactionObj = UncheckedUnwrap(reactionObj); + if (JS_IsDeadWrapper(reactionObj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEAD_OBJECT); + return false; + } + MOZ_RELEASE_ASSERT(reactionObj->is<PromiseReactionRecord>()); + ar.emplace(cx, reactionObj); + } + + // Optimized/special cases. + Handle<PromiseReactionRecord*> reaction = + reactionObj.as<PromiseReactionRecord>(); + if (reaction->isDefaultResolvingHandler()) { + return DefaultResolvingPromiseReactionJob(cx, reaction); + } + if (reaction->isAsyncFunction()) { + return AsyncFunctionPromiseReactionJob(cx, reaction); + } + if (reaction->isAsyncGenerator()) { + RootedValue argument(cx, reaction->handlerArg()); + Rooted<AsyncGeneratorObject*> generator(cx, reaction->asyncGenerator()); + auto handler = static_cast<PromiseHandler>(reaction->handler().toInt32()); + return AsyncGeneratorPromiseReactionJob(cx, handler, generator, argument); + } + if (reaction->isDebuggerDummy()) { + return true; + } + + // Step 1.a. Let promiseCapability be reaction.[[Capability]]. + // (implicit) + + // Step 1.c. Let handler be reaction.[[Handler]]. + RootedValue handlerVal(cx, reaction->handler()); + + RootedValue argument(cx, reaction->handlerArg()); + + RootedValue handlerResult(cx); + ResolutionMode resolutionMode = ResolveMode; + + Rooted<SavedFrame*> unwrappedRejectionStack(cx); + + // Step 1.d. If handler is empty, then + if (handlerVal.isInt32()) { + // Step 1.b. Let type be reaction.[[Type]]. + // (reordered) + auto handlerNum = static_cast<PromiseHandler>(handlerVal.toInt32()); + + // Step 1.d.i. If type is Fulfill, let handlerResult be + // NormalCompletion(argument). + if (handlerNum == PromiseHandler::Identity) { + handlerResult = argument; + } else if (handlerNum == PromiseHandler::Thrower) { + // Step 1.d.ii. Else, + // Step 1.d.ii.1. Assert: type is Reject. + // Step 1.d.ii.2. Let handlerResult be ThrowCompletion(argument). + resolutionMode = RejectMode; + handlerResult = argument; + } else { + // Special case for Async-from-Sync Iterator. + + MOZ_ASSERT(handlerNum == + PromiseHandler::AsyncFromSyncIteratorValueUnwrapDone || + handlerNum == + PromiseHandler::AsyncFromSyncIteratorValueUnwrapNotDone); + + bool done = + handlerNum == PromiseHandler::AsyncFromSyncIteratorValueUnwrapDone; + // 25.1.4.2.5 Async-from-Sync Iterator Value Unwrap Functions, steps 1-2. + PlainObject* resultObj = CreateIterResultObject(cx, argument, done); + if (!resultObj) { + return false; + } + + handlerResult = ObjectValue(*resultObj); + } + } else { + MOZ_ASSERT(handlerVal.isObject()); + MOZ_ASSERT(IsCallable(handlerVal)); + + // Step 1.e. Else, let handlerResult be + // Completion(HostCallJobCallback(handler, undefined, + // « argument »)). + if (!Call(cx, handlerVal, UndefinedHandleValue, argument, &handlerResult)) { + resolutionMode = RejectMode; + if (!MaybeGetAndClearExceptionAndStack(cx, &handlerResult, + &unwrappedRejectionStack)) { + return false; + } + } + } + + // Steps 1.f-i. + RootedObject promiseObj(cx, reaction->promise()); + RootedObject callee(cx); + if (resolutionMode == ResolveMode) { + callee = + reaction->getFixedSlot(ReactionRecordSlot_Resolve).toObjectOrNull(); + + return CallPromiseResolveFunction(cx, callee, handlerResult, promiseObj); + } + + callee = reaction->getFixedSlot(ReactionRecordSlot_Reject).toObjectOrNull(); + + return CallPromiseRejectFunction(cx, callee, handlerResult, promiseObj, + unwrappedRejectionStack, + reaction->unhandledRejectionBehavior()); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * NewPromiseResolveThenableJob ( promiseToResolve, thenable, then ) + * https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob + * + * Steps 1.a-d. + * + * A PromiseResolveThenableJob is set as the native function of an extended + * JSFunction object, with all information required for the job's + * execution stored in the function's extended slots. + * + * Usage of the function's extended slots is described in the ThenableJobSlots + * enum. + */ +static bool PromiseResolveThenableJob(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedFunction job(cx, &args.callee().as<JSFunction>()); + RootedValue then(cx, job->getExtendedSlot(ThenableJobSlot_Handler)); + MOZ_ASSERT(then.isObject()); + Rooted<NativeObject*> jobArgs(cx, + &job->getExtendedSlot(ThenableJobSlot_JobData) + .toObject() + .as<NativeObject>()); + + RootedObject promise( + cx, &jobArgs->getDenseElement(ThenableJobDataIndex_Promise).toObject()); + RootedValue thenable(cx, + jobArgs->getDenseElement(ThenableJobDataIndex_Thenable)); + + // Step 1.a. Let resolvingFunctions be + // CreateResolvingFunctions(promiseToResolve). + RootedObject resolveFn(cx); + RootedObject rejectFn(cx); + if (!CreateResolvingFunctions(cx, promise, &resolveFn, &rejectFn)) { + return false; + } + + // Step 1.b. Let thenCallResult be + // HostCallJobCallback(then, thenable, + // « resolvingFunctions.[[Resolve]], + // resolvingFunctions.[[Reject]] »). + FixedInvokeArgs<2> args2(cx); + args2[0].setObject(*resolveFn); + args2[1].setObject(*rejectFn); + + // In difference to the usual pattern, we return immediately on success. + RootedValue rval(cx); + if (Call(cx, then, thenable, args2, &rval)) { + // Step 1.d. Return Completion(thenCallResult). + return true; + } + + // Step 1.c. If thenCallResult is an abrupt completion, then + + Rooted<SavedFrame*> stack(cx); + if (!MaybeGetAndClearExceptionAndStack(cx, &rval, &stack)) { + return false; + } + + // Step 1.c.i. Let status be + // Call(resolvingFunctions.[[Reject]], undefined, + // « thenCallResult.[[Value]] »). + // Step 1.c.ii. Return Completion(status). + RootedValue rejectVal(cx, ObjectValue(*rejectFn)); + return Call(cx, rejectVal, UndefinedHandleValue, rval, &rval); +} + +[[nodiscard]] static bool OriginalPromiseThenWithoutSettleHandlers( + JSContext* cx, Handle<PromiseObject*> promise, + Handle<PromiseObject*> promiseToResolve); + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * NewPromiseResolveThenableJob ( promiseToResolve, thenable, then ) + * https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob + * + * Step 1.a-d. + * + * Specialization of PromiseResolveThenableJob when the `thenable` is a + * built-in Promise object and the `then` property is the built-in + * `Promise.prototype.then` function. + * + * A PromiseResolveBuiltinThenableJob is set as the native function of an + * extended JSFunction object, with all information required for the job's + * execution stored in the function's extended slots. + * + * Usage of the function's extended slots is described in the + * BuiltinThenableJobSlots enum. + */ +static bool PromiseResolveBuiltinThenableJob(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedFunction job(cx, &args.callee().as<JSFunction>()); + RootedObject promise( + cx, &job->getExtendedSlot(BuiltinThenableJobSlot_Promise).toObject()); + RootedObject thenable( + cx, &job->getExtendedSlot(BuiltinThenableJobSlot_Thenable).toObject()); + + cx->check(promise, thenable); + MOZ_ASSERT(promise->is<PromiseObject>()); + MOZ_ASSERT(thenable->is<PromiseObject>()); + + // Step 1.a. Let resolvingFunctions be + // CreateResolvingFunctions(promiseToResolve). + // (skipped) + + // Step 1.b. Let thenCallResult be HostCallJobCallback( + // then, thenable, + // « resolvingFunctions.[[Resolve]], + // resolvingFunctions.[[Reject]] »). + // + // NOTE: In difference to the usual pattern, we return immediately on success. + if (OriginalPromiseThenWithoutSettleHandlers(cx, thenable.as<PromiseObject>(), + promise.as<PromiseObject>())) { + // Step 1.d. Return Completion(thenCallResult). + return true; + } + + // Step 1.c. If thenCallResult is an abrupt completion, then + RootedValue exception(cx); + Rooted<SavedFrame*> stack(cx); + if (!MaybeGetAndClearExceptionAndStack(cx, &exception, &stack)) { + return false; + } + + // Testing functions allow to directly settle a promise without going + // through the resolving functions. In that case the normal bookkeeping to + // ensure only pending promises can be resolved doesn't apply and we need + // to manually check for already settled promises. The exception is simply + // dropped when this case happens. + if (promise->as<PromiseObject>().state() != JS::PromiseState::Pending) { + return true; + } + + // Step 1.c.i. Let status be + // Call(resolvingFunctions.[[Reject]], undefined, + // « thenCallResult.[[Value]] »). + // Step 1.c.ii. Return Completion(status). + return RejectPromiseInternal(cx, promise.as<PromiseObject>(), exception, + stack); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * NewPromiseResolveThenableJob ( promiseToResolve, thenable, then ) + * https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob + * HostEnqueuePromiseJob ( job, realm ) + * https://tc39.es/ecma262/#sec-hostenqueuepromisejob + * + * Tells the embedding to enqueue a Promise resolve thenable job, based on + * three parameters: + * promiseToResolve_ - The promise to resolve, obviously. + * thenable_ - The thenable to resolve the Promise with. + * thenVal - The `then` function to invoke with the `thenable` as the receiver. + */ +[[nodiscard]] static bool EnqueuePromiseResolveThenableJob( + JSContext* cx, HandleValue promiseToResolve_, HandleValue thenable_, + HandleValue thenVal) { + // Need to re-root these to enable wrapping them below. + RootedValue promiseToResolve(cx, promiseToResolve_); + RootedValue thenable(cx, thenable_); + + // Step 2. Let getThenRealmResult be GetFunctionRealm(then.[[Callback]]). + // Step 3. If getThenRealmResult is a normal completion, let thenRealm be + // getThenRealmResult.[[Value]]. + // Step 4. Else, let thenRealm be the current Realm Record. + // Step 5. NOTE: thenRealm is never null. When then.[[Callback]] is a revoked + // Proxy and no code runs, thenRealm is used to create error objects. + // + // NOTE: Instead of passing job and realm separately, we use the job's + // JSFunction object's realm as the job's realm. + // So we should enter the thenRealm before creating the job function. + // + // GetFunctionRealm performed inside AutoFunctionOrCurrentRealm uses checked + // unwrap and this is fine given the behavior difference (see the comment + // around AutoFunctionOrCurrentRealm usage in EnqueuePromiseReactionJob for + // more details) is observable only when the `thenable` is from content realm + // and `then` is from chrome realm, that shouldn't happen in practice. + // + // NOTE: If `thenable` is also from chrome realm, accessing `then` silently + // fails and it returns `undefined`, and that case doesn't reach here. + RootedObject then(cx, &thenVal.toObject()); + AutoFunctionOrCurrentRealm ar(cx, then); + if (then->maybeCCWRealm() != cx->realm()) { + if (!cx->compartment()->wrap(cx, &then)) { + return false; + } + } + + // Wrap the `promiseToResolve` and `thenable` arguments. + if (!cx->compartment()->wrap(cx, &promiseToResolve)) { + return false; + } + + MOZ_ASSERT(thenable.isObject()); + if (!cx->compartment()->wrap(cx, &thenable)) { + return false; + } + + // Step 1. Let job be a new Job Abstract Closure with no parameters that + // captures promiseToResolve, thenable, and then and performs the + // following steps when called: + Handle<PropertyName*> funName = cx->names().empty; + RootedFunction job( + cx, NewNativeFunction(cx, PromiseResolveThenableJob, 0, funName, + gc::AllocKind::FUNCTION_EXTENDED, GenericObject)); + if (!job) { + return false; + } + + // Store the `then` function on the callback. + job->setExtendedSlot(ThenableJobSlot_Handler, ObjectValue(*then)); + + // Create a dense array to hold the data needed for the reaction job to + // work. + // The layout is described in the ThenableJobDataIndices enum. + Rooted<ArrayObject*> data( + cx, NewDenseFullyAllocatedArray(cx, ThenableJobDataLength)); + if (!data) { + return false; + } + + // Set the `promiseToResolve` and `thenable` arguments. + data->setDenseInitializedLength(ThenableJobDataLength); + data->initDenseElement(ThenableJobDataIndex_Promise, promiseToResolve); + data->initDenseElement(ThenableJobDataIndex_Thenable, thenable); + + // Store the data array on the reaction job. + job->setExtendedSlot(ThenableJobSlot_JobData, ObjectValue(*data)); + + // At this point the promise is guaranteed to be wrapped into the job's + // compartment. + RootedObject promise(cx, &promiseToResolve.toObject()); + + Rooted<GlobalObject*> incumbentGlobal(cx, + cx->runtime()->getIncumbentGlobal(cx)); + + // Step X. HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). + return cx->runtime()->enqueuePromiseJob(cx, job, promise, incumbentGlobal); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * NewPromiseResolveThenableJob ( promiseToResolve, thenable, then ) + * https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob + * HostEnqueuePromiseJob ( job, realm ) + * https://tc39.es/ecma262/#sec-hostenqueuepromisejob + * + * Tells the embedding to enqueue a Promise resolve thenable built-in job, + * based on two parameters: + * promiseToResolve - The promise to resolve, obviously. + * thenable - The thenable to resolve the Promise with. + */ +[[nodiscard]] static bool EnqueuePromiseResolveThenableBuiltinJob( + JSContext* cx, HandleObject promiseToResolve, HandleObject thenable) { + cx->check(promiseToResolve, thenable); + MOZ_ASSERT(promiseToResolve->is<PromiseObject>()); + MOZ_ASSERT(thenable->is<PromiseObject>()); + + // Step 1. Let job be a new Job Abstract Closure with no parameters that + // captures promiseToResolve, thenable, and then and performs the + // following steps when called: + Handle<PropertyName*> funName = cx->names().empty; + RootedFunction job( + cx, NewNativeFunction(cx, PromiseResolveBuiltinThenableJob, 0, funName, + gc::AllocKind::FUNCTION_EXTENDED, GenericObject)); + if (!job) { + return false; + } + + // Steps 2-5. + // (implicit) + // `then` is built-in Promise.prototype.then in the current realm., + // thus `thenRealm` is also current realm, and we have nothing to do here. + + // Store the promise and the thenable on the reaction job. + job->setExtendedSlot(BuiltinThenableJobSlot_Promise, + ObjectValue(*promiseToResolve)); + job->setExtendedSlot(BuiltinThenableJobSlot_Thenable, ObjectValue(*thenable)); + + Rooted<GlobalObject*> incumbentGlobal(cx, + cx->runtime()->getIncumbentGlobal(cx)); + + // HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). + return cx->runtime()->enqueuePromiseJob(cx, job, promiseToResolve, + incumbentGlobal); +} + +[[nodiscard]] static bool AddDummyPromiseReactionForDebugger( + JSContext* cx, Handle<PromiseObject*> promise, + HandleObject dependentPromise); + +[[nodiscard]] static bool AddPromiseReaction( + JSContext* cx, Handle<PromiseObject*> promise, + Handle<PromiseReactionRecord*> reaction); + +static JSFunction* GetResolveFunctionFromReject(JSFunction* reject) { + MOZ_ASSERT(reject->maybeNative() == RejectPromiseFunction); + Value resolveFunVal = + reject->getExtendedSlot(RejectFunctionSlot_ResolveFunction); + MOZ_ASSERT(IsNativeFunction(resolveFunVal, ResolvePromiseFunction)); + return &resolveFunVal.toObject().as<JSFunction>(); +} + +static JSFunction* GetRejectFunctionFromResolve(JSFunction* resolve) { + MOZ_ASSERT(resolve->maybeNative() == ResolvePromiseFunction); + Value rejectFunVal = + resolve->getExtendedSlot(ResolveFunctionSlot_RejectFunction); + MOZ_ASSERT(IsNativeFunction(rejectFunVal, RejectPromiseFunction)); + return &rejectFunVal.toObject().as<JSFunction>(); +} + +static JSFunction* GetResolveFunctionFromPromise(PromiseObject* promise) { + Value rejectFunVal = promise->getFixedSlot(PromiseSlot_RejectFunction); + if (rejectFunVal.isUndefined()) { + return nullptr; + } + JSObject* rejectFunObj = &rejectFunVal.toObject(); + + // We can safely unwrap it because all we want is to get the resolve + // function. + if (IsWrapper(rejectFunObj)) { + rejectFunObj = UncheckedUnwrap(rejectFunObj); + } + + if (!rejectFunObj->is<JSFunction>()) { + return nullptr; + } + + JSFunction* rejectFun = &rejectFunObj->as<JSFunction>(); + + // Only the original RejectPromiseFunction has a reference to the resolve + // function. + if (rejectFun->maybeNative() != &RejectPromiseFunction) { + return nullptr; + } + + // The reject function was already called and cleared its resolve-function + // extended slot. + if (rejectFun->getExtendedSlot(RejectFunctionSlot_ResolveFunction) + .isUndefined()) { + return nullptr; + } + + return GetResolveFunctionFromReject(rejectFun); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise ( executor ) + * https://tc39.es/ecma262/#sec-promise-executor + * + * Steps 3-7. + */ +[[nodiscard]] static MOZ_ALWAYS_INLINE PromiseObject* +CreatePromiseObjectInternal(JSContext* cx, HandleObject proto /* = nullptr */, + bool protoIsWrapped /* = false */, + bool informDebugger /* = true */) { + // Enter the unwrapped proto's compartment, if that's different from + // the current one. + // All state stored in a Promise's fixed slots must be created in the + // same compartment, so we get all of that out of the way here. + // (Except for the resolution functions, which are created below.) + mozilla::Maybe<AutoRealm> ar; + if (protoIsWrapped) { + ar.emplace(cx, proto); + } + + // Step 3. Let promise be + // ? OrdinaryCreateFromConstructor( + // NewTarget, "%Promise.prototype%", + // « [[PromiseState]], [[PromiseResult]], + // [[PromiseFulfillReactions]], [[PromiseRejectReactions]], + // [[PromiseIsHandled]] »). + PromiseObject* promise = NewObjectWithClassProto<PromiseObject>(cx, proto); + if (!promise) { + return nullptr; + } + + // Step 4. Set promise.[[PromiseState]] to pending. + promise->initFixedSlot(PromiseSlot_Flags, Int32Value(0)); + + // Step 5. Set promise.[[PromiseFulfillReactions]] to a new empty List. + // Step 6. Set promise.[[PromiseRejectReactions]] to a new empty List. + // (omitted) + // We allocate our single list of reaction records lazily. + + // Step 7. Set promise.[[PromiseIsHandled]] to false. + // (implicit) + // The handled flag is unset by default. + + if (MOZ_LIKELY(!JS::IsAsyncStackCaptureEnabledForRealm(cx))) { + return promise; + } + + // Store an allocation stack so we can later figure out what the + // control flow was for some unexpected results. Frightfully expensive, + // but oh well. + + Rooted<PromiseObject*> promiseRoot(cx, promise); + + PromiseDebugInfo* debugInfo = PromiseDebugInfo::create(cx, promiseRoot); + if (!debugInfo) { + return nullptr; + } + + // Let the Debugger know about this Promise. + if (informDebugger) { + DebugAPI::onNewPromise(cx, promiseRoot); + } + + return promiseRoot; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise ( executor ) + * https://tc39.es/ecma262/#sec-promise-executor + */ +static bool PromiseConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. If NewTarget is undefined, throw a TypeError exception. + if (!ThrowIfNotConstructing(cx, args, "Promise")) { + return false; + } + + // Step 2. If IsCallable(executor) is false, throw a TypeError exception. + HandleValue executorVal = args.get(0); + if (!IsCallable(executorVal)) { + return ReportIsNotFunction(cx, executorVal); + } + RootedObject executor(cx, &executorVal.toObject()); + + RootedObject newTarget(cx, &args.newTarget().toObject()); + + // If the constructor is called via an Xray wrapper, then the newTarget + // hasn't been unwrapped. We want that because, while the actual instance + // should be created in the target compartment, the constructor's code + // should run in the wrapper's compartment. + // + // This is so that the resolve and reject callbacks get created in the + // wrapper's compartment, which is required for code in that compartment + // to freely interact with it, and, e.g., pass objects as arguments, which + // it wouldn't be able to if the callbacks were themselves wrapped in Xray + // wrappers. + // + // At the same time, just creating the Promise itself in the wrapper's + // compartment wouldn't be helpful: if the wrapper forbids interactions + // with objects except for specific actions, such as calling them, then + // the code we want to expose it to can't actually treat it as a Promise: + // calling .then on it would throw, for example. + // + // Another scenario where it's important to create the Promise in a + // different compartment from the resolution functions is when we want to + // give non-privileged code a Promise resolved with the result of a + // Promise from privileged code; as a return value of a JS-implemented + // API, say. If the resolution functions were unprivileged, then resolving + // with a privileged Promise would cause `resolve` to attempt accessing + // .then on the passed Promise, which would throw an exception, so we'd + // just end up with a rejected Promise. Really, we want to chain the two + // Promises, with the unprivileged one resolved with the resolution of the + // privileged one. + + bool needsWrapping = false; + RootedObject proto(cx); + if (IsWrapper(newTarget)) { + JSObject* unwrappedNewTarget = CheckedUnwrapStatic(newTarget); + MOZ_ASSERT(unwrappedNewTarget); + MOZ_ASSERT(unwrappedNewTarget != newTarget); + + newTarget = unwrappedNewTarget; + { + AutoRealm ar(cx, newTarget); + Handle<GlobalObject*> global = cx->global(); + JSObject* promiseCtor = + GlobalObject::getOrCreatePromiseConstructor(cx, global); + if (!promiseCtor) { + return false; + } + + // Promise subclasses don't get the special Xray treatment, so + // we only need to do the complex wrapping and unwrapping scheme + // described above for instances of Promise itself. + if (newTarget == promiseCtor) { + needsWrapping = true; + proto = GlobalObject::getOrCreatePromisePrototype(cx, cx->global()); + if (!proto) { + return false; + } + } + } + } + + if (needsWrapping) { + if (!cx->compartment()->wrap(cx, &proto)) { + return false; + } + } else { + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Promise, + &proto)) { + return false; + } + } + PromiseObject* promise = + PromiseObject::create(cx, executor, proto, needsWrapping); + if (!promise) { + return false; + } + + // Step 11. + args.rval().setObject(*promise); + if (needsWrapping) { + return cx->compartment()->wrap(cx, args.rval()); + } + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise ( executor ) + * https://tc39.es/ecma262/#sec-promise-executor + * + * Steps 3-11. + */ +/* static */ +PromiseObject* PromiseObject::create(JSContext* cx, HandleObject executor, + HandleObject proto /* = nullptr */, + bool needsWrapping /* = false */) { + MOZ_ASSERT(executor->isCallable()); + + RootedObject usedProto(cx, proto); + // If the proto is wrapped, that means the current function is running + // with a different compartment active from the one the Promise instance + // is to be created in. + // See the comment in PromiseConstructor for details. + if (needsWrapping) { + MOZ_ASSERT(proto); + usedProto = CheckedUnwrapStatic(proto); + if (!usedProto) { + ReportAccessDenied(cx); + return nullptr; + } + } + + // Steps 3-7. + Rooted<PromiseObject*> promise( + cx, CreatePromiseObjectInternal(cx, usedProto, needsWrapping, false)); + if (!promise) { + return nullptr; + } + + RootedObject promiseObj(cx, promise); + if (needsWrapping && !cx->compartment()->wrap(cx, &promiseObj)) { + return nullptr; + } + + // Step 8. Let resolvingFunctions be CreateResolvingFunctions(promise). + // + // The resolving functions are created in the compartment active when the + // (maybe wrapped) Promise constructor was called. They contain checks and + // can unwrap the Promise if required. + RootedObject resolveFn(cx); + RootedObject rejectFn(cx); + if (!CreateResolvingFunctions(cx, promiseObj, &resolveFn, &rejectFn)) { + return nullptr; + } + + // Need to wrap the resolution functions before storing them on the Promise. + MOZ_ASSERT(promise->getFixedSlot(PromiseSlot_RejectFunction).isUndefined(), + "Slot must be undefined so initFixedSlot can be used"); + if (needsWrapping) { + AutoRealm ar(cx, promise); + RootedObject wrappedRejectFn(cx, rejectFn); + if (!cx->compartment()->wrap(cx, &wrappedRejectFn)) { + return nullptr; + } + promise->initFixedSlot(PromiseSlot_RejectFunction, + ObjectValue(*wrappedRejectFn)); + } else { + promise->initFixedSlot(PromiseSlot_RejectFunction, ObjectValue(*rejectFn)); + } + + // Step 9. Let completion be + // Call(executor, undefined, « resolvingFunctions.[[Resolve]], + // resolvingFunctions.[[Reject]] »). + bool success; + { + FixedInvokeArgs<2> args(cx); + args[0].setObject(*resolveFn); + args[1].setObject(*rejectFn); + + RootedValue calleeOrRval(cx, ObjectValue(*executor)); + success = Call(cx, calleeOrRval, UndefinedHandleValue, args, &calleeOrRval); + } + + // Step 10. If completion is an abrupt completion, then + if (!success) { + RootedValue exceptionVal(cx); + Rooted<SavedFrame*> stack(cx); + if (!MaybeGetAndClearExceptionAndStack(cx, &exceptionVal, &stack)) { + return nullptr; + } + + // Step 10.a. Perform + // ? Call(resolvingFunctions.[[Reject]], undefined, + // « completion.[[Value]] »). + RootedValue calleeOrRval(cx, ObjectValue(*rejectFn)); + if (!Call(cx, calleeOrRval, UndefinedHandleValue, exceptionVal, + &calleeOrRval)) { + return nullptr; + } + } + + // Let the Debugger know about this Promise. + DebugAPI::onNewPromise(cx, promise); + + // Step 11. Return promise. + return promise; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise ( executor ) + * https://tc39.es/ecma262/#sec-promise-executor + * + * skipping creation of resolution functions and executor function invocation. + */ +/* static */ +PromiseObject* PromiseObject::createSkippingExecutor(JSContext* cx) { + return CreatePromiseObjectWithoutResolutionFunctions(cx); +} + +class MOZ_STACK_CLASS PromiseForOfIterator : public JS::ForOfIterator { + public: + using JS::ForOfIterator::ForOfIterator; + + bool isOptimizedDenseArrayIteration() { + MOZ_ASSERT(valueIsIterable()); + return index != NOT_ARRAY && IsPackedArray(iterator); + } +}; + +[[nodiscard]] static bool PerformPromiseAll( + JSContext* cx, PromiseForOfIterator& iterator, HandleObject C, + Handle<PromiseCapability> resultCapability, HandleValue promiseResolve, + bool* done); + +[[nodiscard]] static bool PerformPromiseAllSettled( + JSContext* cx, PromiseForOfIterator& iterator, HandleObject C, + Handle<PromiseCapability> resultCapability, HandleValue promiseResolve, + bool* done); + +[[nodiscard]] static bool PerformPromiseAny( + JSContext* cx, PromiseForOfIterator& iterator, HandleObject C, + Handle<PromiseCapability> resultCapability, HandleValue promiseResolve, + bool* done); + +[[nodiscard]] static bool PerformPromiseRace( + JSContext* cx, PromiseForOfIterator& iterator, HandleObject C, + Handle<PromiseCapability> resultCapability, HandleValue promiseResolve, + bool* done); + +enum class CombinatorKind { All, AllSettled, Any, Race }; + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Unified implementation of + * + * Promise.all ( iterable ) + * https://tc39.es/ecma262/#sec-promise.all + * Promise.allSettled ( iterable ) + * https://tc39.es/ecma262/#sec-promise.allsettled + * Promise.race ( iterable ) + * https://tc39.es/ecma262/#sec-promise.race + * Promise.any ( iterable ) + * https://tc39.es/ecma262/#sec-promise.any + * GetPromiseResolve ( promiseConstructor ) + * https://tc39.es/ecma262/#sec-getpromiseresolve + */ +[[nodiscard]] static bool CommonPromiseCombinator(JSContext* cx, CallArgs& args, + CombinatorKind kind) { + HandleValue iterable = args.get(0); + + // Step 2. Let promiseCapability be ? NewPromiseCapability(C). + // (moved from NewPromiseCapability, step 1). + HandleValue CVal = args.thisv(); + if (!CVal.isObject()) { + const char* message; + switch (kind) { + case CombinatorKind::All: + message = "Receiver of Promise.all call"; + break; + case CombinatorKind::AllSettled: + message = "Receiver of Promise.allSettled call"; + break; + case CombinatorKind::Any: + message = "Receiver of Promise.any call"; + break; + case CombinatorKind::Race: + message = "Receiver of Promise.race call"; + break; + } + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_OBJECT_REQUIRED, message); + return false; + } + + // Step 1. Let C be the this value. + RootedObject C(cx, &CVal.toObject()); + + // Step 2. Let promiseCapability be ? NewPromiseCapability(C). + Rooted<PromiseCapability> promiseCapability(cx); + if (!NewPromiseCapability(cx, C, &promiseCapability, false)) { + return false; + } + + RootedValue promiseResolve(cx, UndefinedValue()); + { + JSObject* promiseCtor = + GlobalObject::getOrCreatePromiseConstructor(cx, cx->global()); + if (!promiseCtor) { + return false; + } + + PromiseLookup& promiseLookup = cx->realm()->promiseLookup; + if (C != promiseCtor || !promiseLookup.isDefaultPromiseState(cx)) { + // Step 3. Let promiseResolve be GetPromiseResolve(C). + + // GetPromiseResolve + // Step 1. Let promiseResolve be ? Get(promiseConstructor, "resolve"). + if (!GetProperty(cx, C, C, cx->names().resolve, &promiseResolve)) { + // Step 4. IfAbruptRejectPromise(promiseResolve, promiseCapability). + return AbruptRejectPromise(cx, args, promiseCapability); + } + + // GetPromiseResolve + // Step 2. If IsCallable(promiseResolve) is false, + // throw a TypeError exception. + if (!IsCallable(promiseResolve)) { + ReportIsNotFunction(cx, promiseResolve); + + // Step 4. IfAbruptRejectPromise(promiseResolve, promiseCapability). + return AbruptRejectPromise(cx, args, promiseCapability); + } + } + } + + // Step 5. Let iteratorRecord be GetIterator(iterable). + PromiseForOfIterator iter(cx); + if (!iter.init(iterable, JS::ForOfIterator::AllowNonIterable)) { + // Step 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability). + return AbruptRejectPromise(cx, args, promiseCapability); + } + + if (!iter.valueIsIterable()) { + // Step 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability). + const char* message; + switch (kind) { + case CombinatorKind::All: + message = "Argument of Promise.all"; + break; + case CombinatorKind::AllSettled: + message = "Argument of Promise.allSettled"; + break; + case CombinatorKind::Any: + message = "Argument of Promise.any"; + break; + case CombinatorKind::Race: + message = "Argument of Promise.race"; + break; + } + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_ITERABLE, + message); + return AbruptRejectPromise(cx, args, promiseCapability); + } + + bool done, result; + switch (kind) { + case CombinatorKind::All: + // Promise.all + // Step 7. Let result be + // PerformPromiseAll(iteratorRecord, C, promiseCapability, + // promiseResolve). + result = PerformPromiseAll(cx, iter, C, promiseCapability, promiseResolve, + &done); + break; + case CombinatorKind::AllSettled: + // Promise.allSettled + // Step 7. Let result be + // PerformPromiseAllSettled(iteratorRecord, C, promiseCapability, + // promiseResolve). + result = PerformPromiseAllSettled(cx, iter, C, promiseCapability, + promiseResolve, &done); + break; + case CombinatorKind::Any: + // Promise.any + // Step 7. Let result be + // PerformPromiseAny(iteratorRecord, C, promiseCapability, + // promiseResolve). + result = PerformPromiseAny(cx, iter, C, promiseCapability, promiseResolve, + &done); + break; + case CombinatorKind::Race: + // Promise.race + // Step 7. Let result be + // PerformPromiseRace(iteratorRecord, C, promiseCapability, + // promiseResolve). + result = PerformPromiseRace(cx, iter, C, promiseCapability, + promiseResolve, &done); + break; + } + + // Step 8. If result is an abrupt completion, then + if (!result) { + // Step 8.a. If iteratorRecord.[[Done]] is false, + // set result to IteratorClose(iteratorRecord, result). + if (!done) { + iter.closeThrow(); + } + + // Step 8.b. IfAbruptRejectPromise(result, promiseCapability). + return AbruptRejectPromise(cx, args, promiseCapability); + } + + // Step 9. Return Completion(result). + args.rval().setObject(*promiseCapability.promise()); + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.all ( iterable ) + * https://tc39.es/ecma262/#sec-promise.all + */ +static bool Promise_static_all(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CommonPromiseCombinator(cx, args, CombinatorKind::All); +} + +[[nodiscard]] static bool PerformPromiseThen( + JSContext* cx, Handle<PromiseObject*> promise, HandleValue onFulfilled_, + HandleValue onRejected_, Handle<PromiseCapability> resultCapability); + +[[nodiscard]] static bool PerformPromiseThenWithoutSettleHandlers( + JSContext* cx, Handle<PromiseObject*> promise, + Handle<PromiseObject*> promiseToResolve, + Handle<PromiseCapability> resultCapability); + +static JSFunction* NewPromiseCombinatorElementFunction( + JSContext* cx, Native native, + Handle<PromiseCombinatorDataHolder*> dataHolder, uint32_t index); + +static bool PromiseAllResolveElementFunction(JSContext* cx, unsigned argc, + Value* vp); + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.all ( iterable ) + * https://tc39.es/ecma262/#sec-promise.all + * PerformPromiseAll ( iteratorRecord, constructor, resultCapability, + * promiseResolve ) + * https://tc39.es/ecma262/#sec-performpromiseall + * + * Unforgeable version. + */ +[[nodiscard]] JSObject* js::GetWaitForAllPromise( + JSContext* cx, JS::HandleObjectVector promises) { +#ifdef DEBUG + for (size_t i = 0, len = promises.length(); i < len; i++) { + JSObject* obj = promises[i]; + cx->check(obj); + MOZ_ASSERT(UncheckedUnwrap(obj)->is<PromiseObject>()); + } +#endif + + // Step 1. Let C be the this value. + RootedObject C(cx, + GlobalObject::getOrCreatePromiseConstructor(cx, cx->global())); + if (!C) { + return nullptr; + } + + // Step 2. Let promiseCapability be ? NewPromiseCapability(C). + Rooted<PromiseCapability> resultCapability(cx); + if (!NewPromiseCapability(cx, C, &resultCapability, false)) { + return nullptr; + } + + // Steps 3-6 for iterator and iteratorRecord. + // (omitted) + + // Step 7. Let result be + // PerformPromiseAll(iteratorRecord, C, promiseCapability, + // promiseResolve). + // + // Implemented as an inlined, simplied version of PerformPromiseAll. + { + uint32_t promiseCount = promises.length(); + // PerformPromiseAll + + // Step 1. Let values be a new empty List. + Rooted<PromiseCombinatorElements> values(cx); + { + auto* valuesArray = NewDenseFullyAllocatedArray(cx, promiseCount); + if (!valuesArray) { + return nullptr; + } + valuesArray->ensureDenseInitializedLength(0, promiseCount); + + values.initialize(valuesArray); + } + + // Step 2. Let remainingElementsCount be the Record { [[Value]]: 1 }. + // + // Create our data holder that holds all the things shared across + // every step of the iterator. In particular, this holds the + // remainingElementsCount (as an integer reserved slot), the array of + // values, and the resolve function from our PromiseCapability. + Rooted<PromiseCombinatorDataHolder*> dataHolder(cx); + dataHolder = PromiseCombinatorDataHolder::New( + cx, resultCapability.promise(), values, resultCapability.resolve()); + if (!dataHolder) { + return nullptr; + } + + // Call PerformPromiseThen with resolve and reject set to nullptr. + Rooted<PromiseCapability> resultCapabilityWithoutResolving(cx); + resultCapabilityWithoutResolving.promise().set(resultCapability.promise()); + + // Step 3. Let index be 0. + // Step 4. Repeat, + // Step 4.t. Set index to index + 1. + for (uint32_t index = 0; index < promiseCount; index++) { + // Steps 4.a-c for IteratorStep. + // (omitted) + + // Step 4.d. (implemented after the loop). + + // Steps 4.e-g for IteratorValue + // (omitted) + + // Step 4.h. Append undefined to values. + values.unwrappedArray()->setDenseElement(index, UndefinedHandleValue); + + // Step 4.i. Let nextPromise be + // ? Call(promiseResolve, constructor, « nextValue »). + RootedObject nextPromiseObj(cx, promises[index]); + + // Steps 4.j-q. + JSFunction* resolveFunc = NewPromiseCombinatorElementFunction( + cx, PromiseAllResolveElementFunction, dataHolder, index); + if (!resolveFunc) { + return nullptr; + } + + // Step 4.r. Set remainingElementsCount.[[Value]] to + // remainingElementsCount.[[Value]] + 1. + dataHolder->increaseRemainingCount(); + + // Step 4.s. Perform + // ? Invoke(nextPromise, "then", + // « onFulfilled, resultCapability.[[Reject]] »). + RootedValue resolveFunVal(cx, ObjectValue(*resolveFunc)); + RootedValue rejectFunVal(cx, ObjectValue(*resultCapability.reject())); + Rooted<PromiseObject*> nextPromise(cx); + + // GetWaitForAllPromise is used internally only and must not + // trigger content-observable effects when registering a reaction. + // It's also meant to work on wrapped Promises, potentially from + // compartments with principals inaccessible from the current + // compartment. To make that work, it unwraps promises with + // UncheckedUnwrap, + nextPromise = &UncheckedUnwrap(nextPromiseObj)->as<PromiseObject>(); + + if (!PerformPromiseThen(cx, nextPromise, resolveFunVal, rejectFunVal, + resultCapabilityWithoutResolving)) { + return nullptr; + } + } + + // Step 4.d.i. Set iteratorRecord.[[Done]] to true. + // (implicit) + + // Step 4.d.ii. Set remainingElementsCount.[[Value]] to + // remainingElementsCount.[[Value]] - 1. + int32_t remainingCount = dataHolder->decreaseRemainingCount(); + + // Step 4.d.iii.If remainingElementsCount.[[Value]] is 0, then + if (remainingCount == 0) { + // Step 4.d.iii.1. Let valuesArray be ! CreateArrayFromList(values). + // (already performed) + + // Step 4.d.iii.2. Perform + // ? Call(resultCapability.[[Resolve]], undefined, + // « valuesArray »). + if (!ResolvePromiseInternal(cx, resultCapability.promise(), + values.value())) { + return nullptr; + } + } + } + + // Step 4.d.iv. Return resultCapability.[[Promise]]. + return resultCapability.promise(); +} + +static bool CallDefaultPromiseResolveFunction(JSContext* cx, + Handle<PromiseObject*> promise, + HandleValue resolutionValue); +static bool CallDefaultPromiseRejectFunction( + JSContext* cx, Handle<PromiseObject*> promise, HandleValue rejectionValue, + JS::Handle<SavedFrame*> unwrappedRejectionStack = nullptr); + +/** + * Perform Call(promiseCapability.[[Resolve]], undefined ,« value ») given + * promiseCapability = { promiseObj, resolveFun }. + * + * Also, + * + * ES2023 draft rev 714fa3dd1e8237ae9c666146270f81880089eca5 + * + * NewPromiseReactionJob ( reaction, argument ) + * https://tc39.es/ecma262/#sec-newpromisereactionjob + * + * Steps 1.f-i. "type is Fulfill" case. + */ +[[nodiscard]] static bool CallPromiseResolveFunction(JSContext* cx, + HandleObject resolveFun, + HandleValue value, + HandleObject promiseObj) { + cx->check(resolveFun); + cx->check(value); + cx->check(promiseObj); + + // NewPromiseReactionJob + // Step 1.g. Assert: promiseCapability is a PromiseCapability Record. + // (implicit) + + if (resolveFun) { + // NewPromiseReactionJob + // Step 1.h. If handlerResult is an abrupt completion, then + // (handled in CallPromiseRejectFunction) + // Step 1.i. Else, + // Step 1.i.i. Return + // ? Call(promiseCapability.[[Resolve]], undefined, + // « handlerResult.[[Value]] »). + RootedValue calleeOrRval(cx, ObjectValue(*resolveFun)); + return Call(cx, calleeOrRval, UndefinedHandleValue, value, &calleeOrRval); + } + + // `promiseObj` can be optimized away if it's known to be unused. + // + // NewPromiseReactionJob + // Step f. If promiseCapability is undefined, then + // (reordered) + // + // NOTE: "promiseCapability is undefined" case is represented by + // `resolveFun == nullptr && promiseObj == nullptr`. + if (!promiseObj) { + // NewPromiseReactionJob + // Step f.i. Assert: handlerResult is not an abrupt completion. + // (implicit) + + // Step f.ii. Return empty. + return true; + } + + // NewPromiseReactionJob + // Step 1.h. If handlerResult is an abrupt completion, then + // (handled in CallPromiseRejectFunction) + // Step 1.i. Else, + // Step 1.i.i. Return + // ? Call(promiseCapability.[[Resolve]], undefined, + // « handlerResult.[[Value]] »). + Handle<PromiseObject*> promise = promiseObj.as<PromiseObject>(); + if (IsPromiseWithDefaultResolvingFunction(promise)) { + return CallDefaultPromiseResolveFunction(cx, promise, value); + } + + // This case is used by resultCapabilityWithoutResolving in + // GetWaitForAllPromise, and nothing should be done. + + return true; +} + +/** + * Perform Call(promiseCapability.[[Reject]], undefined ,« reason ») given + * promiseCapability = { promiseObj, rejectFun }. + * + * Also, + * + * ES2023 draft rev 714fa3dd1e8237ae9c666146270f81880089eca5 + * + * NewPromiseReactionJob ( reaction, argument ) + * https://tc39.es/ecma262/#sec-newpromisereactionjob + * + * Steps 1.g-i. "type is Reject" case. + */ +[[nodiscard]] static bool CallPromiseRejectFunction( + JSContext* cx, HandleObject rejectFun, HandleValue reason, + HandleObject promiseObj, Handle<SavedFrame*> unwrappedRejectionStack, + UnhandledRejectionBehavior behavior) { + cx->check(rejectFun); + cx->check(reason); + cx->check(promiseObj); + + // NewPromiseReactionJob + // Step 1.g. Assert: promiseCapability is a PromiseCapability Record. + // (implicit) + + if (rejectFun) { + // NewPromiseReactionJob + // Step 1.h. If handlerResult is an abrupt completion, then + // Step 1.h.i. Return + // ? Call(promiseCapability.[[Reject]], undefined, + // « handlerResult.[[Value]] »). + RootedValue calleeOrRval(cx, ObjectValue(*rejectFun)); + return Call(cx, calleeOrRval, UndefinedHandleValue, reason, &calleeOrRval); + } + + // NewPromiseReactionJob + // See the comment in CallPromiseResolveFunction for promiseCapability field + // + // Step f. If promiseCapability is undefined, then + // Step f.i. Assert: handlerResult is not an abrupt completion. + // + // The spec doesn't allow promiseCapability to be undefined for reject case, + // but `promiseObj` can be optimized away if it's known to be unused. + if (!promiseObj) { + if (behavior == UnhandledRejectionBehavior::Ignore) { + // Do nothing if unhandled rejections are to be ignored. + return true; + } + + // Otherwise create and reject a promise on the fly. The promise's + // allocation time will be wrong. So it goes. + Rooted<PromiseObject*> temporaryPromise( + cx, CreatePromiseObjectWithoutResolutionFunctions(cx)); + if (!temporaryPromise) { + cx->clearPendingException(); + return true; + } + + // NewPromiseReactionJob + // Step 1.h. If handlerResult is an abrupt completion, then + // Step 1.h.i. Return + // ? Call(promiseCapability.[[Reject]], undefined, + // « handlerResult.[[Value]] »). + return RejectPromiseInternal(cx, temporaryPromise, reason, + unwrappedRejectionStack); + } + + // NewPromiseReactionJob + // Step 1.h. If handlerResult is an abrupt completion, then + // Step 1.h.i. Return + // ? Call(promiseCapability.[[Reject]], undefined, + // « handlerResult.[[Value]] »). + Handle<PromiseObject*> promise = promiseObj.as<PromiseObject>(); + if (IsPromiseWithDefaultResolvingFunction(promise)) { + return CallDefaultPromiseRejectFunction(cx, promise, reason, + unwrappedRejectionStack); + } + + // This case is used by resultCapabilityWithoutResolving in + // GetWaitForAllPromise, and nothing should be done. + + return true; +} + +[[nodiscard]] static JSObject* CommonStaticResolveRejectImpl( + JSContext* cx, HandleValue thisVal, HandleValue argVal, + ResolutionMode mode); + +static bool IsPromiseSpecies(JSContext* cx, JSFunction* species); + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Unified implementation of + * + * PerformPromiseAll ( iteratorRecord, constructor, resultCapability, + * promiseResolve ) + * https://tc39.es/ecma262/#sec-performpromiseall + * PerformPromiseAllSettled ( iteratorRecord, constructor, resultCapability, + * promiseResolve ) + * https://tc39.es/ecma262/#sec-performpromiseallsettled + * PerformPromiseRace ( iteratorRecord, constructor, resultCapability, + * promiseResolve ) + * https://tc39.es/ecma262/#sec-performpromiserace + * PerformPromiseAny ( iteratorRecord, constructor, resultCapability, + * promiseResolve ) + * https://tc39.es/ecma262/#sec-performpromiseany + * + * Promise.prototype.then ( onFulfilled, onRejected ) + * https://tc39.es/ecma262/#sec-promise.prototype.then + */ +template <typename T> +[[nodiscard]] static bool CommonPerformPromiseCombinator( + JSContext* cx, PromiseForOfIterator& iterator, HandleObject C, + HandleObject resultPromise, HandleValue promiseResolve, bool* done, + bool resolveReturnsUndefined, T getResolveAndReject) { + RootedObject promiseCtor( + cx, GlobalObject::getOrCreatePromiseConstructor(cx, cx->global())); + if (!promiseCtor) { + return false; + } + + // Optimized dense array iteration ensures no side-effects take place + // during the iteration. + bool iterationMayHaveSideEffects = !iterator.isOptimizedDenseArrayIteration(); + + PromiseLookup& promiseLookup = cx->realm()->promiseLookup; + + // Try to optimize when the Promise object is in its default state, guarded + // by |C == promiseCtor| because we can only perform this optimization + // for the builtin Promise constructor. + bool isDefaultPromiseState = + C == promiseCtor && promiseLookup.isDefaultPromiseState(cx); + bool validatePromiseState = iterationMayHaveSideEffects; + + RootedValue CVal(cx, ObjectValue(*C)); + RootedValue resolveFunVal(cx); + RootedValue rejectFunVal(cx); + + // We're reusing rooted variables in the loop below, so we don't need to + // declare a gazillion different rooted variables here. Rooted variables + // which are reused include "Or" in their name. + RootedValue nextValueOrNextPromise(cx); + RootedObject nextPromiseObj(cx); + RootedValue thenVal(cx); + RootedObject thenSpeciesOrBlockedPromise(cx); + Rooted<PromiseCapability> thenCapability(cx); + + // PerformPromiseAll, PerformPromiseAllSettled, PerformPromiseAny + // Step 4. + // PerformPromiseRace + // Step 1. + while (true) { + // Step a. Let next be IteratorStep(iteratorRecord). + // Step b. If next is an abrupt completion, set iteratorRecord.[[Done]] to + // true. + // Step c. ReturnIfAbrupt(next). + // Step e. Let nextValue be IteratorValue(next). + // Step f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] + // to true. + // Step g. ReturnIfAbrupt(nextValue). + RootedValue& nextValue = nextValueOrNextPromise; + if (!iterator.next(&nextValue, done)) { + *done = true; + return false; + } + + // Step d. If next is false, then + if (*done) { + return true; + } + + // Set to false when we can skip the [[Get]] for "then" and instead + // use the built-in Promise.prototype.then function. + bool getThen = true; + + if (isDefaultPromiseState && validatePromiseState) { + isDefaultPromiseState = promiseLookup.isDefaultPromiseState(cx); + } + + RootedValue& nextPromise = nextValueOrNextPromise; + if (isDefaultPromiseState) { + PromiseObject* nextValuePromise = nullptr; + if (nextValue.isObject() && nextValue.toObject().is<PromiseObject>()) { + nextValuePromise = &nextValue.toObject().as<PromiseObject>(); + } + + if (nextValuePromise && + promiseLookup.isDefaultInstanceWhenPromiseStateIsSane( + cx, nextValuePromise)) { + // The below steps don't produce any side-effects, so we can + // skip the Promise state revalidation in the next iteration + // when the iterator itself also doesn't produce any + // side-effects. + validatePromiseState = iterationMayHaveSideEffects; + + // Step {i, h}. Let nextPromise be + // ? Call(promiseResolve, constructor, « nextValue »). + // Promise.resolve is a no-op for the default case. + MOZ_ASSERT(&nextPromise.toObject() == nextValuePromise); + + // `nextPromise` uses the built-in `then` function. + getThen = false; + } else { + // Need to revalidate the Promise state in the next iteration, + // because CommonStaticResolveRejectImpl may have modified it. + validatePromiseState = true; + + // Step {i, h}. Let nextPromise be + // ? Call(promiseResolve, constructor, « nextValue »). + // Inline the call to Promise.resolve. + JSObject* res = + CommonStaticResolveRejectImpl(cx, CVal, nextValue, ResolveMode); + if (!res) { + return false; + } + + nextPromise.setObject(*res); + } + } else if (promiseResolve.isUndefined()) { + // |promiseResolve| is undefined when the Promise constructor was + // initially in its default state, i.e. if it had been retrieved, it would + // have been set to |Promise.resolve|. + + // Step {i, h}. Let nextPromise be + // ? Call(promiseResolve, constructor, « nextValue »). + // Inline the call to Promise.resolve. + JSObject* res = + CommonStaticResolveRejectImpl(cx, CVal, nextValue, ResolveMode); + if (!res) { + return false; + } + + nextPromise.setObject(*res); + } else { + // Step {i, h}. Let nextPromise be + // ? Call(promiseResolve, constructor, « nextValue »). + if (!Call(cx, promiseResolve, CVal, nextValue, &nextPromise)) { + return false; + } + } + + // Get the resolving functions for this iteration. + // PerformPromiseAll + // Steps j-r. + // PerformPromiseAllSettled + // Steps j-aa. + // PerformPromiseRace + // Step i. + // PerformPromiseAny + // Steps j-q. + if (!getResolveAndReject(&resolveFunVal, &rejectFunVal)) { + return false; + } + + // Call |nextPromise.then| with the provided hooks and add + // |resultPromise| to the list of dependent promises. + // + // If |nextPromise.then| is the original |Promise.prototype.then| + // function and the call to |nextPromise.then| would use the original + // |Promise| constructor to create the resulting promise, we skip the + // call to |nextPromise.then| and thus creating a new promise that + // would not be observable by content. + + // PerformPromiseAll + // Step s. Perform + // ? Invoke(nextPromise, "then", + // « onFulfilled, resultCapability.[[Reject]] »). + // PerformPromiseAllSettled + // Step ab. Perform + // ? Invoke(nextPromise, "then", « onFulfilled, onRejected »). + // PerformPromiseRace + // Step i. Perform + // ? Invoke(nextPromise, "then", + // « resultCapability.[[Resolve]], + // resultCapability.[[Reject]] »). + // PerformPromiseAny + // Step s. Perform + // ? Invoke(nextPromise, "then", + // « resultCapability.[[Resolve]], onRejected »). + nextPromiseObj = ToObject(cx, nextPromise); + if (!nextPromiseObj) { + return false; + } + + bool isBuiltinThen; + if (getThen) { + // We don't use the Promise lookup cache here, because this code + // is only called when we had a lookup cache miss, so it's likely + // we'd get another cache miss when trying to use the cache here. + if (!GetProperty(cx, nextPromiseObj, nextPromise, cx->names().then, + &thenVal)) { + return false; + } + + // |nextPromise| is an unwrapped Promise, and |then| is the + // original |Promise.prototype.then|, inline it here. + isBuiltinThen = nextPromiseObj->is<PromiseObject>() && + IsNativeFunction(thenVal, Promise_then); + } else { + isBuiltinThen = true; + } + + // By default, the blocked promise is added as an extra entry to the + // rejected promises list. + bool addToDependent = true; + + if (isBuiltinThen) { + MOZ_ASSERT(nextPromise.isObject()); + MOZ_ASSERT(&nextPromise.toObject() == nextPromiseObj); + + // Promise.prototype.then + // Step 3. Let C be ? SpeciesConstructor(promise, %Promise%). + RootedObject& thenSpecies = thenSpeciesOrBlockedPromise; + if (getThen) { + thenSpecies = SpeciesConstructor(cx, nextPromiseObj, JSProto_Promise, + IsPromiseSpecies); + if (!thenSpecies) { + return false; + } + } else { + thenSpecies = promiseCtor; + } + + // The fast path here and the one in NewPromiseCapability may not + // set the resolve and reject handlers, so we need to clear the + // fields in case they were set in the previous iteration. + thenCapability.resolve().set(nullptr); + thenCapability.reject().set(nullptr); + + // Skip the creation of a built-in Promise object if: + // 1. `thenSpecies` is the built-in Promise constructor. + // 2. `resolveFun` doesn't return an object, which ensures no side effects + // occur in ResolvePromiseInternal. + // 3. The result promise is a built-in Promise object. + // 4. The result promise doesn't use the default resolving functions, + // which in turn means Run{Fulfill,Reject}Function when called from + // PromiseReactionJob won't try to resolve the promise. + if (thenSpecies == promiseCtor && resolveReturnsUndefined && + resultPromise->is<PromiseObject>() && + !IsPromiseWithDefaultResolvingFunction( + &resultPromise->as<PromiseObject>())) { + thenCapability.promise().set(resultPromise); + addToDependent = false; + } else { + // Promise.prototype.then + // Step 4. Let resultCapability be ? NewPromiseCapability(C). + if (!NewPromiseCapability(cx, thenSpecies, &thenCapability, true)) { + return false; + } + } + + // Promise.prototype.then + // Step 5. Return + // PerformPromiseThen(promise, onFulfilled, onRejected, + // resultCapability). + Handle<PromiseObject*> promise = nextPromiseObj.as<PromiseObject>(); + if (!PerformPromiseThen(cx, promise, resolveFunVal, rejectFunVal, + thenCapability)) { + return false; + } + } else { + // Optimization failed, do the normal call. + RootedValue& ignored = thenVal; + if (!Call(cx, thenVal, nextPromise, resolveFunVal, rejectFunVal, + &ignored)) { + return false; + } + + // In case the value to depend on isn't an object at all, there's + // nothing more to do here: we can only add reactions to Promise + // objects (potentially after unwrapping them), and non-object + // values can't be Promise objects. This can happen if Promise.all + // is called on an object with a `resolve` method that returns + // primitives. + if (!nextPromise.isObject()) { + addToDependent = false; + } + } + + // Adds |resultPromise| to the list of dependent promises. + if (addToDependent) { + // The object created by the |promise.then| call or the inlined + // version of it above is visible to content (either because + // |promise.then| was overridden by content and could leak it, + // or because a constructor other than the original value of + // |Promise| was used to create it). To have both that object and + // |resultPromise| show up as dependent promises in the debugger, + // add a dummy reaction to the list of reject reactions that + // contains |resultPromise|, but otherwise does nothing. + RootedObject& blockedPromise = thenSpeciesOrBlockedPromise; + blockedPromise = resultPromise; + + mozilla::Maybe<AutoRealm> ar; + if (IsProxy(nextPromiseObj)) { + nextPromiseObj = CheckedUnwrapStatic(nextPromiseObj); + if (!nextPromiseObj) { + ReportAccessDenied(cx); + return false; + } + if (JS_IsDeadWrapper(nextPromiseObj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEAD_OBJECT); + return false; + } + ar.emplace(cx, nextPromiseObj); + if (!cx->compartment()->wrap(cx, &blockedPromise)) { + return false; + } + } + + // If either the object to depend on (`nextPromiseObj`) or the + // object that gets blocked (`resultPromise`) isn't a, + // maybe-wrapped, Promise instance, we ignore it. All this does is + // lose some small amount of debug information in scenarios that + // are highly unlikely to occur in useful code. + if (nextPromiseObj->is<PromiseObject>() && + resultPromise->is<PromiseObject>()) { + Handle<PromiseObject*> promise = nextPromiseObj.as<PromiseObject>(); + if (!AddDummyPromiseReactionForDebugger(cx, promise, blockedPromise)) { + return false; + } + } + } + } +} + +// Create the elements for the Promise combinators Promise.all and +// Promise.allSettled. +[[nodiscard]] static bool NewPromiseCombinatorElements( + JSContext* cx, Handle<PromiseCapability> resultCapability, + MutableHandle<PromiseCombinatorElements> elements) { + // We have to be very careful about which compartments we create things for + // the Promise combinators. In particular, we have to maintain the invariant + // that anything stored in a reserved slot is same-compartment with the object + // whose reserved slot it's in. But we want to create the values array in the + // compartment of the result capability's Promise, because that array can get + // exposed as the Promise's resolution value to code that has access to the + // Promise (in particular code from that compartment), and that should work, + // even if the Promise compartment is less-privileged than our caller + // compartment. + // + // So the plan is as follows: Create the values array in the promise + // compartment. Create the promise resolving functions and the data holder in + // our current compartment, i.e. the compartment of the Promise combinator + // function. Store a cross-compartment wrapper to the values array in the + // holder. This should be OK because the only things we hand the promise + // resolving functions to are the "then" calls we do and in the case when the + // Promise's compartment is not the current compartment those are happening + // over Xrays anyway, which means they get the canonical "then" function and + // content can't see our promise resolving functions. + + if (IsWrapper(resultCapability.promise())) { + JSObject* unwrappedPromiseObj = + CheckedUnwrapStatic(resultCapability.promise()); + MOZ_ASSERT(unwrappedPromiseObj); + + { + AutoRealm ar(cx, unwrappedPromiseObj); + auto* array = NewDenseEmptyArray(cx); + if (!array) { + return false; + } + elements.initialize(array); + } + + if (!cx->compartment()->wrap(cx, elements.value())) { + return false; + } + } else { + auto* array = NewDenseEmptyArray(cx); + if (!array) { + return false; + } + + elements.initialize(array); + } + return true; +} + +// Retrieve the combinator elements from the data holder. +[[nodiscard]] static bool GetPromiseCombinatorElements( + JSContext* cx, Handle<PromiseCombinatorDataHolder*> data, + MutableHandle<PromiseCombinatorElements> elements) { + bool needsWrapping = false; + JSObject* valuesObj = &data->valuesArray().toObject(); + if (IsProxy(valuesObj)) { + // See comment for NewPromiseCombinatorElements for why we unwrap here. + valuesObj = UncheckedUnwrap(valuesObj); + + if (JS_IsDeadWrapper(valuesObj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEAD_OBJECT); + return false; + } + + needsWrapping = true; + } + + elements.initialize(data, &valuesObj->as<ArrayObject>(), needsWrapping); + return true; +} + +static JSFunction* NewPromiseCombinatorElementFunction( + JSContext* cx, Native native, + Handle<PromiseCombinatorDataHolder*> dataHolder, uint32_t index) { + JSFunction* fn = NewNativeFunction( + cx, native, 1, nullptr, gc::AllocKind::FUNCTION_EXTENDED, GenericObject); + if (!fn) { + return nullptr; + } + + fn->setExtendedSlot(PromiseCombinatorElementFunctionSlot_Data, + ObjectValue(*dataHolder)); + fn->setExtendedSlot(PromiseCombinatorElementFunctionSlot_ElementIndex, + Int32Value(index)); + return fn; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Unified implementation of + * + * Promise.all Resolve Element Functions + * https://tc39.es/ecma262/#sec-promise.all-resolve-element-functions + * + * Steps 1-4. + * + * Promise.allSettled Resolve Element Functions + * https://tc39.es/ecma262/#sec-promise.allsettled-resolve-element-functions + * + * Steps 1-5. + * + * Promise.allSettled Reject Element Functions + * https://tc39.es/ecma262/#sec-promise.allsettled-reject-element-functions + * + * Steps 1-5. + * + * Common implementation for Promise combinator element functions to check if + * they've already been called. + */ +static bool PromiseCombinatorElementFunctionAlreadyCalled( + const CallArgs& args, MutableHandle<PromiseCombinatorDataHolder*> data, + uint32_t* index) { + // Step 1. Let F be the active function object. + JSFunction* fn = &args.callee().as<JSFunction>(); + + // Promise.all functions + // Step 2. If F.[[AlreadyCalled]] is true, return undefined. + // Promise.allSettled functions + // Step 2. Let alreadyCalled be F.[[AlreadyCalled]]. + // Step 3. If alreadyCalled.[[Value]] is true, return undefined. + // + // We use the existence of the data holder as a signal for whether the Promise + // combinator element function was already called. Upon resolution, it's reset + // to `undefined`. + const Value& dataVal = + fn->getExtendedSlot(PromiseCombinatorElementFunctionSlot_Data); + if (dataVal.isUndefined()) { + return true; + } + + data.set(&dataVal.toObject().as<PromiseCombinatorDataHolder>()); + + // Promise.all functions + // Step 3. Set F.[[AlreadyCalled]] to true. + // Promise.allSettled functions + // Step 4. Set alreadyCalled.[[Value]] to true. + fn->setExtendedSlot(PromiseCombinatorElementFunctionSlot_Data, + UndefinedValue()); + + // Promise.all functions + // Step 4. Let index be F.[[Index]]. + // Promise.allSettled functions + // Step 5. Let index be F.[[Index]]. + int32_t idx = + fn->getExtendedSlot(PromiseCombinatorElementFunctionSlot_ElementIndex) + .toInt32(); + MOZ_ASSERT(idx >= 0); + *index = uint32_t(idx); + + return false; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * PerformPromiseAll ( iteratorRecord, constructor, resultCapability, + * promiseResolve ) + * https://tc39.es/ecma262/#sec-performpromiseall + */ +[[nodiscard]] static bool PerformPromiseAll( + JSContext* cx, PromiseForOfIterator& iterator, HandleObject C, + Handle<PromiseCapability> resultCapability, HandleValue promiseResolve, + bool* done) { + *done = false; + + MOZ_ASSERT(C->isConstructor()); + + // Step 1. Let values be a new empty List. + Rooted<PromiseCombinatorElements> values(cx); + if (!NewPromiseCombinatorElements(cx, resultCapability, &values)) { + return false; + } + + // Step 2. Let remainingElementsCount be the Record { [[Value]]: 1 }. + // + // Create our data holder that holds all the things shared across + // every step of the iterator. In particular, this holds the + // remainingElementsCount (as an integer reserved slot), the array of + // values, and the resolve function from our PromiseCapability. + Rooted<PromiseCombinatorDataHolder*> dataHolder(cx); + dataHolder = PromiseCombinatorDataHolder::New( + cx, resultCapability.promise(), values, resultCapability.resolve()); + if (!dataHolder) { + return false; + } + + // Step 3. Let index be 0. + uint32_t index = 0; + + auto getResolveAndReject = [cx, &resultCapability, &values, &dataHolder, + &index](MutableHandleValue resolveFunVal, + MutableHandleValue rejectFunVal) { + // Step 4.h. Append undefined to values. + if (!values.pushUndefined(cx)) { + return false; + } + + // Steps 4.j-q. + JSFunction* resolveFunc = NewPromiseCombinatorElementFunction( + cx, PromiseAllResolveElementFunction, dataHolder, index); + if (!resolveFunc) { + return false; + } + + // Step 4.r. Set remainingElementsCount.[[Value]] to + // remainingElementsCount.[[Value]] + 1. + dataHolder->increaseRemainingCount(); + + // Step 4.t. Set index to index + 1. + index++; + MOZ_ASSERT(index > 0); + + resolveFunVal.setObject(*resolveFunc); + rejectFunVal.setObject(*resultCapability.reject()); + return true; + }; + + // Steps 4. + if (!CommonPerformPromiseCombinator( + cx, iterator, C, resultCapability.promise(), promiseResolve, done, + true, getResolveAndReject)) { + return false; + } + + // Step 4.d.ii. Set remainingElementsCount.[[Value]] to + // remainingElementsCount.[[Value]] - 1. + int32_t remainingCount = dataHolder->decreaseRemainingCount(); + + // Step 4.d.iii. If remainingElementsCount.[[Value]] is 0, then + if (remainingCount == 0) { + // Step 4.d.iii.1. Let valuesArray be ! CreateArrayFromList(values). + // (already performed) + + // Step 4.d.iii.2. Perform + // ? Call(resultCapability.[[Resolve]], undefined, + // « valuesArray »). + return CallPromiseResolveFunction(cx, resultCapability.resolve(), + values.value(), + resultCapability.promise()); + } + + // Step 4.d.iv. Return resultCapability.[[Promise]]. + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.all Resolve Element Functions + * https://tc39.es/ecma262/#sec-promise.all-resolve-element-functions + */ +static bool PromiseAllResolveElementFunction(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue xVal = args.get(0); + + // Steps 1-4. + Rooted<PromiseCombinatorDataHolder*> data(cx); + uint32_t index; + if (PromiseCombinatorElementFunctionAlreadyCalled(args, &data, &index)) { + args.rval().setUndefined(); + return true; + } + + // Step 5. Let values be F.[[Values]]. + Rooted<PromiseCombinatorElements> values(cx); + if (!GetPromiseCombinatorElements(cx, data, &values)) { + return false; + } + + // Step 8. Set values[index] to x. + if (!values.setElement(cx, index, xVal)) { + return false; + } + + // (reordered) + // Step 7. Let remainingElementsCount be F.[[RemainingElements]]. + // + // Step 9. Set remainingElementsCount.[[Value]] to + // remainingElementsCount.[[Value]] - 1. + uint32_t remainingCount = data->decreaseRemainingCount(); + + // Step 10. If remainingElementsCount.[[Value]] is 0, then + if (remainingCount == 0) { + // Step 10.a. Let valuesArray be ! CreateArrayFromList(values). + // (already performed) + + // (reordered) + // Step 6. Let promiseCapability be F.[[Capability]]. + // + // Step 10.b. Return + // ? Call(promiseCapability.[[Resolve]], undefined, + // « valuesArray »). + RootedObject resolveAllFun(cx, data->resolveOrRejectObj()); + RootedObject promiseObj(cx, data->promiseObj()); + if (!CallPromiseResolveFunction(cx, resolveAllFun, values.value(), + promiseObj)) { + return false; + } + } + + // Step 11. Return undefined. + args.rval().setUndefined(); + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.race ( iterable ) + * https://tc39.es/ecma262/#sec-promise.race + */ +static bool Promise_static_race(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CommonPromiseCombinator(cx, args, CombinatorKind::Race); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * PerformPromiseRace ( iteratorRecord, constructor, resultCapability, + * promiseResolve ) + * https://tc39.es/ecma262/#sec-performpromiserace + */ +[[nodiscard]] static bool PerformPromiseRace( + JSContext* cx, PromiseForOfIterator& iterator, HandleObject C, + Handle<PromiseCapability> resultCapability, HandleValue promiseResolve, + bool* done) { + *done = false; + + MOZ_ASSERT(C->isConstructor()); + + // BlockOnPromise fast path requires the passed onFulfilled function + // doesn't return an object value, because otherwise the skipped promise + // creation is detectable due to missing property lookups. + bool isDefaultResolveFn = + IsNativeFunction(resultCapability.resolve(), ResolvePromiseFunction); + + auto getResolveAndReject = [&resultCapability]( + MutableHandleValue resolveFunVal, + MutableHandleValue rejectFunVal) { + resolveFunVal.setObject(*resultCapability.resolve()); + rejectFunVal.setObject(*resultCapability.reject()); + return true; + }; + + // Step 1. + return CommonPerformPromiseCombinator( + cx, iterator, C, resultCapability.promise(), promiseResolve, done, + isDefaultResolveFn, getResolveAndReject); +} + +enum class PromiseAllSettledElementFunctionKind { Resolve, Reject }; + +template <PromiseAllSettledElementFunctionKind Kind> +static bool PromiseAllSettledElementFunction(JSContext* cx, unsigned argc, + Value* vp); + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.allSettled ( iterable ) + * https://tc39.es/ecma262/#sec-promise.allsettled + */ +static bool Promise_static_allSettled(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CommonPromiseCombinator(cx, args, CombinatorKind::AllSettled); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * PerformPromiseAllSettled ( iteratorRecord, constructor, resultCapability, + * promiseResolve ) + * https://tc39.es/ecma262/#sec-performpromiseallsettled + */ +[[nodiscard]] static bool PerformPromiseAllSettled( + JSContext* cx, PromiseForOfIterator& iterator, HandleObject C, + Handle<PromiseCapability> resultCapability, HandleValue promiseResolve, + bool* done) { + *done = false; + + MOZ_ASSERT(C->isConstructor()); + + // Step 1. Let values be a new empty List. + Rooted<PromiseCombinatorElements> values(cx); + if (!NewPromiseCombinatorElements(cx, resultCapability, &values)) { + return false; + } + + // Step 2. Let remainingElementsCount be the Record { [[Value]]: 1 }. + // + // Create our data holder that holds all the things shared across every step + // of the iterator. In particular, this holds the remainingElementsCount + // (as an integer reserved slot), the array of values, and the resolve + // function from our PromiseCapability. + Rooted<PromiseCombinatorDataHolder*> dataHolder(cx); + dataHolder = PromiseCombinatorDataHolder::New( + cx, resultCapability.promise(), values, resultCapability.resolve()); + if (!dataHolder) { + return false; + } + + // Step 3. Let index be 0. + uint32_t index = 0; + + auto getResolveAndReject = [cx, &values, &dataHolder, &index]( + MutableHandleValue resolveFunVal, + MutableHandleValue rejectFunVal) { + // Step 4.h. Append undefined to values. + if (!values.pushUndefined(cx)) { + return false; + } + + auto PromiseAllSettledResolveElementFunction = + PromiseAllSettledElementFunction< + PromiseAllSettledElementFunctionKind::Resolve>; + auto PromiseAllSettledRejectElementFunction = + PromiseAllSettledElementFunction< + PromiseAllSettledElementFunctionKind::Reject>; + + // Steps 4.j-r. + JSFunction* resolveFunc = NewPromiseCombinatorElementFunction( + cx, PromiseAllSettledResolveElementFunction, dataHolder, index); + if (!resolveFunc) { + return false; + } + resolveFunVal.setObject(*resolveFunc); + + // Steps 4.s-z. + JSFunction* rejectFunc = NewPromiseCombinatorElementFunction( + cx, PromiseAllSettledRejectElementFunction, dataHolder, index); + if (!rejectFunc) { + return false; + } + rejectFunVal.setObject(*rejectFunc); + + // Step 4.aa. Set remainingElementsCount.[[Value]] to + // remainingElementsCount.[[Value]] + 1. + dataHolder->increaseRemainingCount(); + + // Step 4.ac. Set index to index + 1. + index++; + MOZ_ASSERT(index > 0); + + return true; + }; + + // Steps 4. + if (!CommonPerformPromiseCombinator( + cx, iterator, C, resultCapability.promise(), promiseResolve, done, + true, getResolveAndReject)) { + return false; + } + + // Step 4.d.ii. Set remainingElementsCount.[[Value]] to + // remainingElementsCount.[[Value]] - 1. + int32_t remainingCount = dataHolder->decreaseRemainingCount(); + + // Step 4.d.iii. If remainingElementsCount.[[Value]] is 0, then + if (remainingCount == 0) { + // Step 4.d.iii.1. Let valuesArray be ! CreateArrayFromList(values). + // (already performed) + + // Step 4.d.iii.2. Perform + // ? Call(resultCapability.[[Resolve]], undefined, + // « valuesArray »). + return CallPromiseResolveFunction(cx, resultCapability.resolve(), + values.value(), + resultCapability.promise()); + } + + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Unified implementation of + * + * Promise.allSettled Resolve Element Functions + * https://tc39.es/ecma262/#sec-promise.allsettled-resolve-element-functions + * Promise.allSettled Reject Element Functions + * https://tc39.es/ecma262/#sec-promise.allsettled-reject-element-functions + */ +template <PromiseAllSettledElementFunctionKind Kind> +static bool PromiseAllSettledElementFunction(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue valueOrReason = args.get(0); + + // Steps 1-5. + Rooted<PromiseCombinatorDataHolder*> data(cx); + uint32_t index; + if (PromiseCombinatorElementFunctionAlreadyCalled(args, &data, &index)) { + args.rval().setUndefined(); + return true; + } + + // Step 6. Let values be F.[[Values]]. + Rooted<PromiseCombinatorElements> values(cx); + if (!GetPromiseCombinatorElements(cx, data, &values)) { + return false; + } + + // Step 2. Let alreadyCalled be F.[[AlreadyCalled]]. + // Step 3. If alreadyCalled.[[Value]] is true, return undefined. + // + // The already-called check above only handles the case when |this| function + // is called repeatedly, so we still need to check if the other pair of this + // resolving function was already called: + // We use the element value as a signal for whether the Promise was already + // fulfilled. Upon resolution, it's set to the result object created below. + if (!values.unwrappedArray()->getDenseElement(index).isUndefined()) { + args.rval().setUndefined(); + return true; + } + + // Step 9. Let obj be ! OrdinaryObjectCreate(%Object.prototype%). + Rooted<PlainObject*> obj(cx, NewPlainObject(cx)); + if (!obj) { + return false; + } + + // Promise.allSettled Resolve Element Functions + // Step 10. Perform ! CreateDataPropertyOrThrow(obj, "status", "fulfilled"). + // Promise.allSettled Reject Element Functions + // Step 10. Perform ! CreateDataPropertyOrThrow(obj, "status", "rejected"). + RootedId id(cx, NameToId(cx->names().status)); + RootedValue statusValue(cx); + if (Kind == PromiseAllSettledElementFunctionKind::Resolve) { + statusValue.setString(cx->names().fulfilled); + } else { + statusValue.setString(cx->names().rejected); + } + if (!NativeDefineDataProperty(cx, obj, id, statusValue, JSPROP_ENUMERATE)) { + return false; + } + + // Promise.allSettled Resolve Element Functions + // Step 11. Perform ! CreateDataPropertyOrThrow(obj, "value", x). + // Promise.allSettled Reject Element Functions + // Step 11. Perform ! CreateDataPropertyOrThrow(obj, "reason", x). + if (Kind == PromiseAllSettledElementFunctionKind::Resolve) { + id = NameToId(cx->names().value); + } else { + id = NameToId(cx->names().reason); + } + if (!NativeDefineDataProperty(cx, obj, id, valueOrReason, JSPROP_ENUMERATE)) { + return false; + } + + // Step 12. Set values[index] to obj. + RootedValue objVal(cx, ObjectValue(*obj)); + if (!values.setElement(cx, index, objVal)) { + return false; + } + + // (reordered) + // Step 8. Let remainingElementsCount be F.[[RemainingElements]]. + // + // Step 13. Set remainingElementsCount.[[Value]] to + // remainingElementsCount.[[Value]] - 1. + uint32_t remainingCount = data->decreaseRemainingCount(); + + // Step 14. If remainingElementsCount.[[Value]] is 0, then + if (remainingCount == 0) { + // Step 14.a. Let valuesArray be ! CreateArrayFromList(values). + // (already performed) + + // (reordered) + // Step 7. Let promiseCapability be F.[[Capability]]. + // + // Step 14.b. Return + // ? Call(promiseCapability.[[Resolve]], undefined, + // « valuesArray »). + RootedObject resolveAllFun(cx, data->resolveOrRejectObj()); + RootedObject promiseObj(cx, data->promiseObj()); + if (!CallPromiseResolveFunction(cx, resolveAllFun, values.value(), + promiseObj)) { + return false; + } + } + + // Step 15. Return undefined. + args.rval().setUndefined(); + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.any ( iterable ) + * https://tc39.es/ecma262/#sec-promise.any + */ +static bool Promise_static_any(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CommonPromiseCombinator(cx, args, CombinatorKind::Any); +} + +static bool PromiseAnyRejectElementFunction(JSContext* cx, unsigned argc, + Value* vp); + +static void ThrowAggregateError(JSContext* cx, + Handle<PromiseCombinatorElements> errors, + HandleObject promise); + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.any ( iterable ) + * https://tc39.es/ecma262/#sec-promise.any + * PerformPromiseAny ( iteratorRecord, constructor, resultCapability, + * promiseResolve ) + * https://tc39.es/ecma262/#sec-performpromiseany + */ +[[nodiscard]] static bool PerformPromiseAny( + JSContext* cx, PromiseForOfIterator& iterator, HandleObject C, + Handle<PromiseCapability> resultCapability, HandleValue promiseResolve, + bool* done) { + *done = false; + + // Step 1. Let C be the this value. + MOZ_ASSERT(C->isConstructor()); + + // Step 2. Let promiseCapability be ? NewPromiseCapability(C). + // (omitted). + + // Step 3. Let promiseResolve be GetPromiseResolve(C). + Rooted<PromiseCombinatorElements> errors(cx); + if (!NewPromiseCombinatorElements(cx, resultCapability, &errors)) { + return false; + } + + // Step 4. + // Create our data holder that holds all the things shared across every step + // of the iterator. In particular, this holds the remainingElementsCount (as + // an integer reserved slot), the array of errors, and the reject function + // from our PromiseCapability. + Rooted<PromiseCombinatorDataHolder*> dataHolder(cx); + dataHolder = PromiseCombinatorDataHolder::New( + cx, resultCapability.promise(), errors, resultCapability.reject()); + if (!dataHolder) { + return false; + } + + // PerformPromiseAny + // Step 3. Let index be 0. + uint32_t index = 0; + + auto getResolveAndReject = [cx, &resultCapability, &errors, &dataHolder, + &index](MutableHandleValue resolveFunVal, + MutableHandleValue rejectFunVal) { + // Step 4.h. Append undefined to errors. + if (!errors.pushUndefined(cx)) { + return false; + } + + // Steps 4.j-q. + JSFunction* rejectFunc = NewPromiseCombinatorElementFunction( + cx, PromiseAnyRejectElementFunction, dataHolder, index); + if (!rejectFunc) { + return false; + } + + // Step 4.r. Set remainingElementsCount.[[Value]] to + // remainingElementsCount.[[Value]] + 1. + dataHolder->increaseRemainingCount(); + + // Step 4.t. Set index to index + 1. + index++; + MOZ_ASSERT(index > 0); + + resolveFunVal.setObject(*resultCapability.resolve()); + rejectFunVal.setObject(*rejectFunc); + return true; + }; + + // BlockOnPromise fast path requires the passed onFulfilled function doesn't + // return an object value, because otherwise the skipped promise creation is + // detectable due to missing property lookups. + bool isDefaultResolveFn = + IsNativeFunction(resultCapability.resolve(), ResolvePromiseFunction); + + // Steps 4. + if (!CommonPerformPromiseCombinator( + cx, iterator, C, resultCapability.promise(), promiseResolve, done, + isDefaultResolveFn, getResolveAndReject)) { + return false; + } + + // Step 4.d.ii. Set remainingElementsCount.[[Value]] to + // remainingElementsCount.[[Value]] - 1. + int32_t remainingCount = dataHolder->decreaseRemainingCount(); + + // Step 4.d.iii. If remainingElementsCount.[[Value]] is 0, then + if (remainingCount == 0) { + ThrowAggregateError(cx, errors, resultCapability.promise()); + return false; + } + + // Step 4.d.iv. Return resultCapability.[[Promise]]. + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.any Reject Element Functions + * https://tc39.es/ecma262/#sec-promise.any-reject-element-functions + */ +static bool PromiseAnyRejectElementFunction(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue xVal = args.get(0); + + // Steps 1-5. + Rooted<PromiseCombinatorDataHolder*> data(cx); + uint32_t index; + if (PromiseCombinatorElementFunctionAlreadyCalled(args, &data, &index)) { + args.rval().setUndefined(); + return true; + } + + // Step 6. + Rooted<PromiseCombinatorElements> errors(cx); + if (!GetPromiseCombinatorElements(cx, data, &errors)) { + return false; + } + + // Step 9. + if (!errors.setElement(cx, index, xVal)) { + return false; + } + + // Steps 8, 10. + uint32_t remainingCount = data->decreaseRemainingCount(); + + // Step 11. + if (remainingCount == 0) { + // Step 7 (Adapted to work with PromiseCombinatorDataHolder's layout). + RootedObject rejectFun(cx, data->resolveOrRejectObj()); + RootedObject promiseObj(cx, data->promiseObj()); + + ThrowAggregateError(cx, errors, promiseObj); + + RootedValue reason(cx); + Rooted<SavedFrame*> stack(cx); + if (!MaybeGetAndClearExceptionAndStack(cx, &reason, &stack)) { + return false; + } + + if (!CallPromiseRejectFunction(cx, rejectFun, reason, promiseObj, stack, + UnhandledRejectionBehavior::Report)) { + return false; + } + } + + // Step 12. + args.rval().setUndefined(); + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * PerformPromiseAny ( iteratorRecord, constructor, resultCapability, + * promiseResolve ) + * https://tc39.es/ecma262/#sec-performpromiseany + * + * Steps 4.d.iii.1-3 + */ +static void ThrowAggregateError(JSContext* cx, + Handle<PromiseCombinatorElements> errors, + HandleObject promise) { + MOZ_ASSERT(!cx->isExceptionPending()); + + // Create the AggregateError in the same realm as the array object. + AutoRealm ar(cx, errors.unwrappedArray()); + + RootedObject allocationSite(cx); + mozilla::Maybe<JS::AutoSetAsyncStackForNewCalls> asyncStack; + + // Provide a more useful error stack if possible: This function is typically + // called from Promise job queue, which doesn't have any JS frames on the + // stack. So when we create the AggregateError below, its stack property will + // be set to the empty string, which makes it harder to debug the error cause. + // To avoid this situation set-up an async stack based on the Promise + // allocation site, which should point to calling site of |Promise.any|. + if (promise->is<PromiseObject>()) { + allocationSite = promise->as<PromiseObject>().allocationSite(); + if (allocationSite) { + asyncStack.emplace( + cx, allocationSite, "Promise.any", + JS::AutoSetAsyncStackForNewCalls::AsyncCallKind::IMPLICIT); + } + } + + // Step 4.d.iii.1. Let error be a newly created AggregateError object. + // + // AutoSetAsyncStackForNewCalls requires a new activation before it takes + // effect, so call into the self-hosting helper to set-up new call frames. + RootedValue error(cx); + if (!GetAggregateError(cx, JSMSG_PROMISE_ANY_REJECTION, &error)) { + return; + } + + // Step 4.d.iii.2. Perform ! DefinePropertyOrThrow( + // error, "errors", PropertyDescriptor { + // [[Configurable]]: true, [[Enumerable]]: false, + // [[Writable]]: true, + // [[Value]]: ! CreateArrayFromList(errors) }). + // + // |error| isn't guaranteed to be an AggregateError in case of OOM or stack + // overflow. + Rooted<SavedFrame*> stack(cx); + if (error.isObject() && error.toObject().is<ErrorObject>()) { + Rooted<ErrorObject*> errorObj(cx, &error.toObject().as<ErrorObject>()); + if (errorObj->type() == JSEXN_AGGREGATEERR) { + RootedValue errorsVal(cx, JS::ObjectValue(*errors.unwrappedArray())); + if (!NativeDefineDataProperty(cx, errorObj, cx->names().errors, errorsVal, + 0)) { + return; + } + + // Adopt the existing saved frames when present. + if (JSObject* errorStack = errorObj->stack()) { + stack = &errorStack->as<SavedFrame>(); + } + } + } + + // Step 4.d.iii.3. Return ThrowCompletion(error). + cx->setPendingException(error, stack); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Unified implementation of + * + * Promise.reject ( r ) + * https://tc39.es/ecma262/#sec-promise.reject + * NewPromiseCapability ( C ) + * https://tc39.es/ecma262/#sec-newpromisecapability + * Promise.resolve ( x ) + * https://tc39.es/ecma262/#sec-promise.resolve + * PromiseResolve ( C, x ) + * https://tc39.es/ecma262/#sec-promise-resolve + */ +[[nodiscard]] static JSObject* CommonStaticResolveRejectImpl( + JSContext* cx, HandleValue thisVal, HandleValue argVal, + ResolutionMode mode) { + // Promise.reject + // Step 1. Let C be the this value. + // Step 2. Let promiseCapability be ? NewPromiseCapability(C). + // + // Promise.reject => NewPromiseCapability + // Step 1. If IsConstructor(C) is false, throw a TypeError exception. + // + // Promise.resolve + // Step 1. Let C be the this value. + // Step 2. If Type(C) is not Object, throw a TypeError exception. + if (!thisVal.isObject()) { + const char* msg = mode == ResolveMode ? "Receiver of Promise.resolve call" + : "Receiver of Promise.reject call"; + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_OBJECT_REQUIRED, msg); + return nullptr; + } + RootedObject C(cx, &thisVal.toObject()); + + // Promise.resolve + // Step 3. Return ? PromiseResolve(C, x). + // + // PromiseResolve + // Step 1. Assert: Type(C) is Object. + // (implicit) + if (mode == ResolveMode && argVal.isObject()) { + RootedObject xObj(cx, &argVal.toObject()); + bool isPromise = false; + if (xObj->is<PromiseObject>()) { + isPromise = true; + } else if (IsWrapper(xObj)) { + // Treat instances of Promise from other compartments as Promises + // here, too. + // It's important to do the GetProperty for the `constructor` + // below through the wrapper, because wrappers can change the + // outcome, so instead of unwrapping and then performing the + // GetProperty, just check here and then operate on the original + // object again. + if (xObj->canUnwrapAs<PromiseObject>()) { + isPromise = true; + } + } + + // PromiseResolve + // Step 2. If IsPromise(x) is true, then + if (isPromise) { + // Step 2.a. Let xConstructor be ? Get(x, "constructor"). + RootedValue ctorVal(cx); + if (!GetProperty(cx, xObj, xObj, cx->names().constructor, &ctorVal)) { + return nullptr; + } + + // Step 2.b. If SameValue(xConstructor, C) is true, return x. + if (ctorVal == thisVal) { + return xObj; + } + } + } + + // Promise.reject + // Step 2. Let promiseCapability be ? NewPromiseCapability(C). + // PromiseResolve + // Step 3. Let promiseCapability be ? NewPromiseCapability(C). + Rooted<PromiseCapability> capability(cx); + if (!NewPromiseCapability(cx, C, &capability, true)) { + return nullptr; + } + + HandleObject promise = capability.promise(); + if (mode == ResolveMode) { + // PromiseResolve + // Step 4. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »). + if (!CallPromiseResolveFunction(cx, capability.resolve(), argVal, + promise)) { + return nullptr; + } + } else { + // Promise.reject + // Step 3. Perform ? Call(promiseCapability.[[Reject]], undefined, « r »). + if (!CallPromiseRejectFunction(cx, capability.reject(), argVal, promise, + nullptr, + UnhandledRejectionBehavior::Report)) { + return nullptr; + } + } + + // Promise.reject + // Step 4. Return promiseCapability.[[Promise]]. + // PromiseResolve + // Step 5. Return promiseCapability.[[Promise]]. + return promise; +} + +[[nodiscard]] JSObject* js::PromiseResolve(JSContext* cx, + HandleObject constructor, + HandleValue value) { + RootedValue C(cx, ObjectValue(*constructor)); + return CommonStaticResolveRejectImpl(cx, C, value, ResolveMode); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.reject ( r ) + * https://tc39.es/ecma262/#sec-promise.reject + */ +static bool Promise_reject(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue thisVal = args.thisv(); + HandleValue argVal = args.get(0); + JSObject* result = + CommonStaticResolveRejectImpl(cx, thisVal, argVal, RejectMode); + if (!result) { + return false; + } + args.rval().setObject(*result); + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.reject ( r ) + * https://tc39.es/ecma262/#sec-promise.reject + * + * Unforgeable version. + */ +/* static */ +PromiseObject* PromiseObject::unforgeableReject(JSContext* cx, + HandleValue value) { + cx->check(value); + + // Step 1. Let C be the this value. + // Step 2. Let promiseCapability be ? NewPromiseCapability(C). + Rooted<PromiseObject*> promise( + cx, CreatePromiseObjectWithoutResolutionFunctions(cx)); + if (!promise) { + return nullptr; + } + + MOZ_ASSERT(promise->state() == JS::PromiseState::Pending); + MOZ_ASSERT(IsPromiseWithDefaultResolvingFunction(promise)); + + // Step 3. Perform ? Call(promiseCapability.[[Reject]], undefined, « r »). + if (!RejectPromiseInternal(cx, promise, value)) { + return nullptr; + } + + // Step 4. Return promiseCapability.[[Promise]]. + return promise; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.resolve ( x ) + * https://tc39.es/ecma262/#sec-promise.resolve + */ +bool js::Promise_static_resolve(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + HandleValue thisVal = args.thisv(); + HandleValue argVal = args.get(0); + JSObject* result = + CommonStaticResolveRejectImpl(cx, thisVal, argVal, ResolveMode); + if (!result) { + return false; + } + args.rval().setObject(*result); + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.resolve ( x ) + * https://tc39.es/ecma262/#sec-promise.resolve + * + * Unforgeable version. + */ +/* static */ +JSObject* PromiseObject::unforgeableResolve(JSContext* cx, HandleValue value) { + JSObject* promiseCtor = JS::GetPromiseConstructor(cx); + if (!promiseCtor) { + return nullptr; + } + RootedValue cVal(cx, ObjectValue(*promiseCtor)); + return CommonStaticResolveRejectImpl(cx, cVal, value, ResolveMode); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.resolve ( x ) + * https://tc39.es/ecma262/#sec-promise.resolve + * PromiseResolve ( C, x ) + * https://tc39.es/ecma262/#sec-promise-resolve + * + * Unforgeable version, where `x` is guaranteed not to be a promise. + */ +/* static */ +PromiseObject* PromiseObject::unforgeableResolveWithNonPromise( + JSContext* cx, HandleValue value) { + cx->check(value); + +#ifdef DEBUG + auto IsPromise = [](HandleValue value) { + if (!value.isObject()) { + return false; + } + + JSObject* obj = &value.toObject(); + if (obj->is<PromiseObject>()) { + return true; + } + + if (!IsWrapper(obj)) { + return false; + } + + return obj->canUnwrapAs<PromiseObject>(); + }; + MOZ_ASSERT(!IsPromise(value), "must use unforgeableResolve with this value"); +#endif + + // Promise.resolve + // Step 3. Return ? PromiseResolve(C, x). + + // PromiseResolve + // Step 2. Let promiseCapability be ? NewPromiseCapability(C). + Rooted<PromiseObject*> promise( + cx, CreatePromiseObjectWithoutResolutionFunctions(cx)); + if (!promise) { + return nullptr; + } + + MOZ_ASSERT(promise->state() == JS::PromiseState::Pending); + MOZ_ASSERT(IsPromiseWithDefaultResolvingFunction(promise)); + + // PromiseResolve + // Step 3. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »). + if (!ResolvePromiseInternal(cx, promise, value)) { + return nullptr; + } + + // PromiseResolve + // Step 4. Return promiseCapability.[[Promise]]. + return promise; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * get Promise [ @@species ] + * https://tc39.es/ecma262/#sec-get-promise-@@species + */ +bool js::Promise_static_species(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. Return the this value. + args.rval().set(args.thisv()); + return true; +} + +enum class IncumbentGlobalObject { + // Do not use the incumbent global, this is a special case used by the + // debugger. + No, + + // Use incumbent global, this is the normal operation. + Yes +}; + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * PerformPromiseThen ( promise, onFulfilled, onRejected + * [ , resultCapability ] ) + * https://tc39.es/ecma262/#sec-performpromisethen + * + * Steps 7-8 for creating PromiseReaction record. + * We use single object for both fulfillReaction and rejectReaction. + */ +static PromiseReactionRecord* NewReactionRecord( + JSContext* cx, Handle<PromiseCapability> resultCapability, + HandleValue onFulfilled, HandleValue onRejected, + IncumbentGlobalObject incumbentGlobalObjectOption) { +#ifdef DEBUG + if (resultCapability.promise()) { + if (incumbentGlobalObjectOption == IncumbentGlobalObject::Yes) { + if (resultCapability.promise()->is<PromiseObject>()) { + // If `resultCapability.promise` is a Promise object, + // `resultCapability.{resolve,reject}` may be optimized out, + // but if they're not, they should be callable. + MOZ_ASSERT_IF(resultCapability.resolve(), + IsCallable(resultCapability.resolve())); + MOZ_ASSERT_IF(resultCapability.reject(), + IsCallable(resultCapability.reject())); + } else { + // If `resultCapability.promise` is a non-Promise object + // (including wrapped Promise object), + // `resultCapability.{resolve,reject}` should be callable. + MOZ_ASSERT(resultCapability.resolve()); + MOZ_ASSERT(IsCallable(resultCapability.resolve())); + MOZ_ASSERT(resultCapability.reject()); + MOZ_ASSERT(IsCallable(resultCapability.reject())); + } + } else { + // For debugger usage, `resultCapability.promise` should be a + // maybe-wrapped Promise object. The other fields are not used. + // + // This is the only case where we allow `resolve` and `reject` to + // be null when the `promise` field is not a PromiseObject. + JSObject* unwrappedPromise = UncheckedUnwrap(resultCapability.promise()); + MOZ_ASSERT(unwrappedPromise->is<PromiseObject>()); + MOZ_ASSERT(!resultCapability.resolve()); + MOZ_ASSERT(!resultCapability.reject()); + } + } else { + // `resultCapability.promise` is null for the following cases: + // * resulting Promise is known to be unused + // * Async Function + // * Async Generator + // In any case, other fields are also not used. + MOZ_ASSERT(!resultCapability.resolve()); + MOZ_ASSERT(!resultCapability.reject()); + MOZ_ASSERT(incumbentGlobalObjectOption == IncumbentGlobalObject::Yes); + } +#endif + + // Ensure the onFulfilled handler has the expected type. + MOZ_ASSERT(onFulfilled.isInt32() || onFulfilled.isObjectOrNull()); + MOZ_ASSERT_IF(onFulfilled.isObject(), IsCallable(onFulfilled)); + MOZ_ASSERT_IF(onFulfilled.isInt32(), + 0 <= onFulfilled.toInt32() && + onFulfilled.toInt32() < int32_t(PromiseHandler::Limit)); + + // Ensure the onRejected handler has the expected type. + MOZ_ASSERT(onRejected.isInt32() || onRejected.isObjectOrNull()); + MOZ_ASSERT_IF(onRejected.isObject(), IsCallable(onRejected)); + MOZ_ASSERT_IF(onRejected.isInt32(), + 0 <= onRejected.toInt32() && + onRejected.toInt32() < int32_t(PromiseHandler::Limit)); + + // Handlers must either both be present or both be absent. + MOZ_ASSERT(onFulfilled.isNull() == onRejected.isNull()); + + RootedObject incumbentGlobalObject(cx); + if (incumbentGlobalObjectOption == IncumbentGlobalObject::Yes) { + if (!GetObjectFromIncumbentGlobal(cx, &incumbentGlobalObject)) { + return nullptr; + } + } + + PromiseReactionRecord* reaction = + NewBuiltinClassInstance<PromiseReactionRecord>(cx); + if (!reaction) { + return nullptr; + } + + cx->check(resultCapability.promise()); + cx->check(onFulfilled); + cx->check(onRejected); + cx->check(resultCapability.resolve()); + cx->check(resultCapability.reject()); + cx->check(incumbentGlobalObject); + + // Step 7. Let fulfillReaction be the PromiseReaction + // { [[Capability]]: resultCapability, [[Type]]: Fulfill, + // [[Handler]]: onFulfilledJobCallback }. + // Step 8. Let rejectReaction be the PromiseReaction + // { [[Capability]]: resultCapability, [[Type]]: Reject, + // [[Handler]]: onRejectedJobCallback }. + + // See comments for ReactionRecordSlots for the relation between + // spec record fields and PromiseReactionRecord slots. + reaction->setFixedSlot(ReactionRecordSlot_Promise, + ObjectOrNullValue(resultCapability.promise())); + // We set [[Type]] in EnqueuePromiseReactionJob, by calling + // setTargetStateAndHandlerArg. + reaction->setFixedSlot(ReactionRecordSlot_Flags, Int32Value(0)); + reaction->setFixedSlot(ReactionRecordSlot_OnFulfilled, onFulfilled); + reaction->setFixedSlot(ReactionRecordSlot_OnRejected, onRejected); + reaction->setFixedSlot(ReactionRecordSlot_Resolve, + ObjectOrNullValue(resultCapability.resolve())); + reaction->setFixedSlot(ReactionRecordSlot_Reject, + ObjectOrNullValue(resultCapability.reject())); + reaction->setFixedSlot(ReactionRecordSlot_IncumbentGlobalObject, + ObjectOrNullValue(incumbentGlobalObject)); + + return reaction; +} + +static bool IsPromiseSpecies(JSContext* cx, JSFunction* species) { + return species->maybeNative() == Promise_static_species; +} + +// Whether to create a promise as the return value of Promise#{then,catch}. +// If the return value is known to be unused, and if the operation is known +// to be unobservable, we can skip creating the promise. +enum class CreateDependentPromise { Always, SkipIfCtorUnobservable }; + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.prototype.then ( onFulfilled, onRejected ) + * https://tc39.es/ecma262/#sec-promise.prototype.then + * + * Steps 3-4. + */ +static bool PromiseThenNewPromiseCapability( + JSContext* cx, HandleObject promiseObj, + CreateDependentPromise createDependent, + MutableHandle<PromiseCapability> resultCapability) { + // Step 3. Let C be ? SpeciesConstructor(promise, %Promise%). + RootedObject C(cx, SpeciesConstructor(cx, promiseObj, JSProto_Promise, + IsPromiseSpecies)); + if (!C) { + return false; + } + + if (createDependent != CreateDependentPromise::Always && + IsNativeFunction(C, PromiseConstructor)) { + return true; + } + + // Step 4. Let resultCapability be ? NewPromiseCapability(C). + if (!NewPromiseCapability(cx, C, resultCapability, true)) { + return false; + } + + RootedObject unwrappedPromise(cx, promiseObj); + if (IsWrapper(promiseObj)) { + unwrappedPromise = UncheckedUnwrap(promiseObj); + } + RootedObject unwrappedNewPromise(cx, resultCapability.promise()); + if (IsWrapper(resultCapability.promise())) { + unwrappedNewPromise = UncheckedUnwrap(resultCapability.promise()); + } + if (unwrappedPromise->is<PromiseObject>() && + unwrappedNewPromise->is<PromiseObject>()) { + unwrappedNewPromise->as<PromiseObject>().copyUserInteractionFlagsFrom( + *unwrappedPromise.as<PromiseObject>()); + } + + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.prototype.then ( onFulfilled, onRejected ) + * https://tc39.es/ecma262/#sec-promise.prototype.then + * + * Steps 3-5. + */ +[[nodiscard]] PromiseObject* js::OriginalPromiseThen(JSContext* cx, + HandleObject promiseObj, + HandleObject onFulfilled, + HandleObject onRejected) { + cx->check(promiseObj); + cx->check(onFulfilled); + cx->check(onRejected); + + RootedValue promiseVal(cx, ObjectValue(*promiseObj)); + Rooted<PromiseObject*> unwrappedPromise( + cx, + UnwrapAndTypeCheckValue<PromiseObject>(cx, promiseVal, [cx, promiseObj] { + JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_PROTO, "Promise", "then", + promiseObj->getClass()->name); + })); + if (!unwrappedPromise) { + return nullptr; + } + + // Step 3. Let C be ? SpeciesConstructor(promise, %Promise%). + // Step 4. Let resultCapability be ? NewPromiseCapability(C). + Rooted<PromiseObject*> newPromise( + cx, CreatePromiseObjectWithoutResolutionFunctions(cx)); + if (!newPromise) { + return nullptr; + } + newPromise->copyUserInteractionFlagsFrom(*unwrappedPromise); + + Rooted<PromiseCapability> resultCapability(cx); + resultCapability.promise().set(newPromise); + + // Step 5. Return PerformPromiseThen(promise, onFulfilled, onRejected, + // resultCapability). + { + RootedValue onFulfilledVal(cx, ObjectOrNullValue(onFulfilled)); + RootedValue onRejectedVal(cx, ObjectOrNullValue(onRejected)); + if (!PerformPromiseThen(cx, unwrappedPromise, onFulfilledVal, onRejectedVal, + resultCapability)) { + return nullptr; + } + } + + return newPromise; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.prototype.then ( onFulfilled, onRejected ) + * https://tc39.es/ecma262/#sec-promise.prototype.then + * + * Steps 3-5. + */ +[[nodiscard]] static bool OriginalPromiseThenWithoutSettleHandlers( + JSContext* cx, Handle<PromiseObject*> promise, + Handle<PromiseObject*> promiseToResolve) { + cx->check(promise); + + // Step 3. Let C be ? SpeciesConstructor(promise, %Promise%). + // Step 4. Let resultCapability be ? NewPromiseCapability(C). + Rooted<PromiseCapability> resultCapability(cx); + if (!PromiseThenNewPromiseCapability( + cx, promise, CreateDependentPromise::SkipIfCtorUnobservable, + &resultCapability)) { + return false; + } + + // Step 5. Return PerformPromiseThen(promise, onFulfilled, onRejected, + // resultCapability). + return PerformPromiseThenWithoutSettleHandlers(cx, promise, promiseToResolve, + resultCapability); +} + +[[nodiscard]] static bool PerformPromiseThenWithReaction( + JSContext* cx, Handle<PromiseObject*> promise, + Handle<PromiseReactionRecord*> reaction); + +[[nodiscard]] bool js::ReactToUnwrappedPromise( + JSContext* cx, Handle<PromiseObject*> unwrappedPromise, + HandleObject onFulfilled_, HandleObject onRejected_, + UnhandledRejectionBehavior behavior) { + cx->check(onFulfilled_); + cx->check(onRejected_); + + MOZ_ASSERT_IF(onFulfilled_, IsCallable(onFulfilled_)); + MOZ_ASSERT_IF(onRejected_, IsCallable(onRejected_)); + + RootedValue onFulfilled( + cx, onFulfilled_ ? ObjectValue(*onFulfilled_) + : Int32Value(int32_t(PromiseHandler::Identity))); + + RootedValue onRejected( + cx, onRejected_ ? ObjectValue(*onRejected_) + : Int32Value(int32_t(PromiseHandler::Thrower))); + + Rooted<PromiseCapability> resultCapability(cx); + MOZ_ASSERT(!resultCapability.promise()); + + Rooted<PromiseReactionRecord*> reaction( + cx, NewReactionRecord(cx, resultCapability, onFulfilled, onRejected, + IncumbentGlobalObject::Yes)); + if (!reaction) { + return false; + } + + if (behavior == UnhandledRejectionBehavior::Ignore) { + reaction->setShouldIgnoreUnhandledRejection(); + } + + return PerformPromiseThenWithReaction(cx, unwrappedPromise, reaction); +} + +static bool CanCallOriginalPromiseThenBuiltin(JSContext* cx, + HandleValue promise) { + return promise.isObject() && promise.toObject().is<PromiseObject>() && + cx->realm()->promiseLookup.isDefaultInstance( + cx, &promise.toObject().as<PromiseObject>()); +} + +static MOZ_ALWAYS_INLINE bool IsPromiseThenOrCatchRetValImplicitlyUsed( + JSContext* cx, PromiseObject* promise) { + // Embedding requires the return value of then/catch as + // `enqueuePromiseJob` parameter, to propaggate the user-interaction. + // We cannot optimize out the return value if the flag is set by embedding. + if (promise->requiresUserInteractionHandling()) { + return true; + } + + // The returned promise of Promise#then and Promise#catch contains + // stack info if async stack is enabled. Even if their return value is not + // used explicitly in the script, the stack info is observable in devtools + // and profilers. We shouldn't apply the optimization not to allocate the + // returned Promise object if the it's implicitly used by them. + if (!cx->options().asyncStack()) { + return false; + } + + // If devtools is opened, the current realm will become debuggee. + if (cx->realm()->isDebuggee()) { + return true; + } + + // There are 2 profilers, and they can be independently enabled. + if (cx->runtime()->geckoProfiler().enabled()) { + return true; + } + if (JS::IsProfileTimelineRecordingEnabled()) { + return true; + } + + // The stack is also observable from Error#stack, but we don't care since + // it's nonstandard feature. + return false; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.prototype.then ( onFulfilled, onRejected ) + * https://tc39.es/ecma262/#sec-promise.prototype.then + * + * Steps 3-5. + */ +static bool OriginalPromiseThenBuiltin(JSContext* cx, HandleValue promiseVal, + HandleValue onFulfilled, + HandleValue onRejected, + MutableHandleValue rval, + bool rvalExplicitlyUsed) { + cx->check(promiseVal, onFulfilled, onRejected); + MOZ_ASSERT(CanCallOriginalPromiseThenBuiltin(cx, promiseVal)); + + Rooted<PromiseObject*> promise(cx, + &promiseVal.toObject().as<PromiseObject>()); + + bool rvalUsed = rvalExplicitlyUsed || + IsPromiseThenOrCatchRetValImplicitlyUsed(cx, promise); + + // Step 3. Let C be ? SpeciesConstructor(promise, %Promise%). + // Step 4. Let resultCapability be ? NewPromiseCapability(C). + Rooted<PromiseCapability> resultCapability(cx); + if (rvalUsed) { + PromiseObject* resultPromise = + CreatePromiseObjectWithoutResolutionFunctions(cx); + if (!resultPromise) { + return false; + } + + resultPromise->copyUserInteractionFlagsFrom( + promiseVal.toObject().as<PromiseObject>()); + resultCapability.promise().set(resultPromise); + } + + // Step 5. Return PerformPromiseThen(promise, onFulfilled, onRejected, + // resultCapability). + if (!PerformPromiseThen(cx, promise, onFulfilled, onRejected, + resultCapability)) { + return false; + } + + if (rvalUsed) { + rval.setObject(*resultCapability.promise()); + } else { + rval.setUndefined(); + } + return true; +} + +[[nodiscard]] bool js::RejectPromiseWithPendingError( + JSContext* cx, Handle<PromiseObject*> promise) { + cx->check(promise); + + if (!cx->isExceptionPending()) { + // Reject the promise, but also propagate this uncatchable error. + (void)PromiseObject::reject(cx, promise, UndefinedHandleValue); + return false; + } + + RootedValue exn(cx); + if (!GetAndClearException(cx, &exn)) { + return false; + } + return PromiseObject::reject(cx, promise, exn); +} + +// Some async/await functions are implemented here instead of +// js/src/builtin/AsyncFunction.cpp, to call Promise internal functions. + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Runtime Semantics: EvaluateAsyncFunctionBody + * AsyncFunctionBody : FunctionBody + * https://tc39.es/ecma262/#sec-runtime-semantics-evaluateasyncfunctionbody + * + * Runtime Semantics: EvaluateAsyncConciseBody + * AsyncConciseBody : ExpressionBody + * https://tc39.es/ecma262/#sec-runtime-semantics-evaluateasyncconcisebody + */ +[[nodiscard]] PromiseObject* js::CreatePromiseObjectForAsync(JSContext* cx) { + // Step 1. Let promiseCapability be ! NewPromiseCapability(%Promise%). + PromiseObject* promise = CreatePromiseObjectWithoutResolutionFunctions(cx); + if (!promise) { + return nullptr; + } + + AddPromiseFlags(*promise, PROMISE_FLAG_ASYNC); + return promise; +} + +bool js::IsPromiseForAsyncFunctionOrGenerator(JSObject* promise) { + return promise->is<PromiseObject>() && + PromiseHasAnyFlag(promise->as<PromiseObject>(), PROMISE_FLAG_ASYNC); +} + +[[nodiscard]] PromiseObject* js::CreatePromiseObjectForAsyncGenerator( + JSContext* cx) { + PromiseObject* promise = CreatePromiseObjectWithoutResolutionFunctions(cx); + if (!promise) { + return nullptr; + } + + AddPromiseFlags(*promise, PROMISE_FLAG_ASYNC); + return promise; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * AsyncFunctionStart ( promiseCapability, asyncFunctionBody ) + * https://tc39.es/ecma262/#sec-async-functions-abstract-operations-async-function-start + * + * Steps 4.f-g. + */ +[[nodiscard]] bool js::AsyncFunctionThrown(JSContext* cx, + Handle<PromiseObject*> resultPromise, + HandleValue reason) { + if (resultPromise->state() != JS::PromiseState::Pending) { + // OOM after resolving promise. + // Report a warning and ignore the result. + if (!WarnNumberASCII(cx, JSMSG_UNHANDLABLE_PROMISE_REJECTION_WARNING)) { + if (cx->isExceptionPending()) { + cx->clearPendingException(); + } + } + return true; + } + + // Step 4.f. Else, + // Step 4.f.i. Assert: result.[[Type]] is throw. + // Step 4.f.ii. Perform + // ! Call(promiseCapability.[[Reject]], undefined, + // « result.[[Value]] »). + // Step 4.g. Return. + return RejectPromiseInternal(cx, resultPromise, reason); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * AsyncFunctionStart ( promiseCapability, asyncFunctionBody ) + * https://tc39.es/ecma262/#sec-async-functions-abstract-operations-async-function-start + * + * Steps 4.e, 4.g. + */ +[[nodiscard]] bool js::AsyncFunctionReturned( + JSContext* cx, Handle<PromiseObject*> resultPromise, HandleValue value) { + // Step 4.e. Else if result.[[Type]] is return, then + // Step 4.e.i. Perform + // ! Call(promiseCapability.[[Resolve]], undefined, + // « result.[[Value]] »). + return ResolvePromiseInternal(cx, resultPromise, value); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Await + * https://tc39.github.io/ecma262/#await + * + * Helper function that performs Await(promise) steps 2-7. + * The same steps are also used in a few other places in the spec. + */ +template <typename T> +[[nodiscard]] static bool InternalAwait(JSContext* cx, HandleValue value, + HandleObject resultPromise, + PromiseHandler onFulfilled, + PromiseHandler onRejected, + T extraStep) { + // Step 2. Let promise be ? PromiseResolve(%Promise%, value). + RootedObject promise(cx, PromiseObject::unforgeableResolve(cx, value)); + if (!promise) { + return false; + } + + // This downcast is safe because unforgeableResolve either returns `value` + // (only if it is already a possibly-wrapped promise) or creates a new + // promise using the Promise constructor. + Rooted<PromiseObject*> unwrappedPromise( + cx, UnwrapAndDowncastObject<PromiseObject>(cx, promise)); + if (!unwrappedPromise) { + return false; + } + + // Steps 3-6 for creating onFulfilled/onRejected are done by caller. + + // Step 7. Perform ! PerformPromiseThen(promise, onFulfilled, onRejected). + RootedValue onFulfilledValue(cx, Int32Value(int32_t(onFulfilled))); + RootedValue onRejectedValue(cx, Int32Value(int32_t(onRejected))); + Rooted<PromiseCapability> resultCapability(cx); + resultCapability.promise().set(resultPromise); + Rooted<PromiseReactionRecord*> reaction( + cx, NewReactionRecord(cx, resultCapability, onFulfilledValue, + onRejectedValue, IncumbentGlobalObject::Yes)); + if (!reaction) { + return false; + } + extraStep(reaction); + return PerformPromiseThenWithReaction(cx, unwrappedPromise, reaction); +} + +[[nodiscard]] bool js::InternalAsyncGeneratorAwait( + JSContext* cx, JS::Handle<AsyncGeneratorObject*> generator, + JS::Handle<JS::Value> value, PromiseHandler onFulfilled, + PromiseHandler onRejected) { + auto extra = [&](Handle<PromiseReactionRecord*> reaction) { + reaction->setIsAsyncGenerator(generator); + }; + return InternalAwait(cx, value, nullptr, onFulfilled, onRejected, extra); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Await + * https://tc39.es/ecma262/#await + */ +[[nodiscard]] JSObject* js::AsyncFunctionAwait( + JSContext* cx, Handle<AsyncFunctionGeneratorObject*> genObj, + HandleValue value) { + auto extra = [&](Handle<PromiseReactionRecord*> reaction) { + reaction->setIsAsyncFunction(genObj); + }; + if (!InternalAwait(cx, value, nullptr, + PromiseHandler::AsyncFunctionAwaitedFulfilled, + PromiseHandler::AsyncFunctionAwaitedRejected, extra)) { + return nullptr; + } + return genObj->promise(); +} + +// https://tc39.github.io/ecma262/#sec-%asyncfromsynciteratorprototype%.next +// 25.1.4.2.1 %AsyncFromSyncIteratorPrototype%.next +// 25.1.4.2.2 %AsyncFromSyncIteratorPrototype%.return +// 25.1.4.2.3 %AsyncFromSyncIteratorPrototype%.throw +bool js::AsyncFromSyncIteratorMethod(JSContext* cx, CallArgs& args, + CompletionKind completionKind) { + // Step 1: Let O be the this value. + HandleValue thisVal = args.thisv(); + + // Step 2: Let promiseCapability be ! NewPromiseCapability(%Promise%). + Rooted<PromiseObject*> resultPromise( + cx, CreatePromiseObjectWithoutResolutionFunctions(cx)); + if (!resultPromise) { + return false; + } + + // Step 3: If Type(O) is not Object, or if O does not have a + // [[SyncIteratorRecord]] internal slot, then + if (!thisVal.isObject() || + !thisVal.toObject().is<AsyncFromSyncIteratorObject>()) { + // NB: See https://github.com/tc39/proposal-async-iteration/issues/105 + // for why this check shouldn't be necessary as long as we can ensure + // the Async-from-Sync iterator can't be accessed directly by user + // code. + + // Step 3.a: Let invalidIteratorError be a newly created TypeError object. + RootedValue badGeneratorError(cx); + if (!GetTypeError(cx, JSMSG_NOT_AN_ASYNC_ITERATOR, &badGeneratorError)) { + return false; + } + + // Step 3.b: Perform ! Call(promiseCapability.[[Reject]], undefined, + // « invalidIteratorError »). + if (!RejectPromiseInternal(cx, resultPromise, badGeneratorError)) { + return false; + } + + // Step 3.c: Return promiseCapability.[[Promise]]. + args.rval().setObject(*resultPromise); + return true; + } + + Rooted<AsyncFromSyncIteratorObject*> asyncIter( + cx, &thisVal.toObject().as<AsyncFromSyncIteratorObject>()); + + // Step 4: Let syncIteratorRecord be O.[[SyncIteratorRecord]]. + RootedObject iter(cx, asyncIter->iterator()); + + RootedValue func(cx); + if (completionKind == CompletionKind::Normal) { + // next() preparing for steps 5-6. + func.set(asyncIter->nextMethod()); + } else if (completionKind == CompletionKind::Return) { + // return() steps 5-7. + // Step 5: Let return be GetMethod(syncIterator, "return"). + // Step 6: IfAbruptRejectPromise(return, promiseCapability). + if (!GetProperty(cx, iter, iter, cx->names().return_, &func)) { + return AbruptRejectPromise(cx, args, resultPromise, nullptr); + } + + // Step 7: If return is undefined, then + // (Note: GetMethod contains a step that changes `null` to `undefined`; + // we omit that step above, and check for `null` here instead.) + if (func.isNullOrUndefined()) { + // Step 7.a: Let iterResult be ! CreateIterResultObject(value, true). + PlainObject* resultObj = CreateIterResultObject(cx, args.get(0), true); + if (!resultObj) { + return AbruptRejectPromise(cx, args, resultPromise, nullptr); + } + + RootedValue resultVal(cx, ObjectValue(*resultObj)); + + // Step 7.b: Perform ! Call(promiseCapability.[[Resolve]], undefined, + // « iterResult »). + if (!ResolvePromiseInternal(cx, resultPromise, resultVal)) { + return AbruptRejectPromise(cx, args, resultPromise, nullptr); + } + + // Step 7.c: Return promiseCapability.[[Promise]]. + args.rval().setObject(*resultPromise); + return true; + } + } else { + // noexcept(true) steps 5-7. + MOZ_ASSERT(completionKind == CompletionKind::Throw); + + // Step 5: Let throw be GetMethod(syncIterator, "throw"). + // Step 6: IfAbruptRejectPromise(throw, promiseCapability). + if (!GetProperty(cx, iter, iter, cx->names().throw_, &func)) { + return AbruptRejectPromise(cx, args, resultPromise, nullptr); + } + + // Step 7: If throw is undefined, then + // (Note: GetMethod contains a step that changes `null` to `undefined`; + // we omit that step above, and check for `null` here instead.) + if (func.isNullOrUndefined()) { + // Step 7.a: Perform ! Call(promiseCapability.[[Reject]], undefined, « + // value »). + if (!RejectPromiseInternal(cx, resultPromise, args.get(0))) { + return AbruptRejectPromise(cx, args, resultPromise, nullptr); + } + + // Step 7.b: Return promiseCapability.[[Promise]]. + args.rval().setObject(*resultPromise); + return true; + } + } + + // next() steps 5-6. + // Step 5: Let result be IteratorNext(syncIteratorRecord, value). + // Step 6: IfAbruptRejectPromise(result, promiseCapability). + // return/throw() steps 8-9. + // Step 8: Let result be Call(throw, syncIterator, « value »). + // Step 9: IfAbruptRejectPromise(result, promiseCapability). + // + // Including the changes from: https://github.com/tc39/ecma262/pull/1776 + RootedValue iterVal(cx, ObjectValue(*iter)); + RootedValue resultVal(cx); + bool ok; + if (args.length() == 0) { + ok = Call(cx, func, iterVal, &resultVal); + } else { + ok = Call(cx, func, iterVal, args[0], &resultVal); + } + if (!ok) { + return AbruptRejectPromise(cx, args, resultPromise, nullptr); + } + + // next() step 5 -> IteratorNext Step 3: + // If Type(result) is not Object, throw a TypeError exception. + // Followed by IfAbruptRejectPromise in step 6. + // + // return/throw() Step 10: If Type(result) is not Object, then + // Step 10.a: Perform ! Call(promiseCapability.[[Reject]], undefined, + // « a newly created TypeError object »). + // Step 10.b: Return promiseCapability.[[Promise]]. + if (!resultVal.isObject()) { + CheckIsObjectKind kind; + switch (completionKind) { + case CompletionKind::Normal: + kind = CheckIsObjectKind::IteratorNext; + break; + case CompletionKind::Throw: + kind = CheckIsObjectKind::IteratorThrow; + break; + case CompletionKind::Return: + kind = CheckIsObjectKind::IteratorReturn; + break; + } + MOZ_ALWAYS_FALSE(ThrowCheckIsObject(cx, kind)); + return AbruptRejectPromise(cx, args, resultPromise, nullptr); + } + + RootedObject resultObj(cx, &resultVal.toObject()); + + // next() Step 7, return/throw() Step 11: Return + // ! AsyncFromSyncIteratorContinuation(result, promiseCapability). + // + // The step numbers below are for + // 25.1.4.4 AsyncFromSyncIteratorContinuation ( result, promiseCapability ). + + // Step 1: Let done be IteratorComplete(result). + // Step 2: IfAbruptRejectPromise(done, promiseCapability). + RootedValue doneVal(cx); + if (!GetProperty(cx, resultObj, resultObj, cx->names().done, &doneVal)) { + return AbruptRejectPromise(cx, args, resultPromise, nullptr); + } + bool done = ToBoolean(doneVal); + + // Step 3: Let value be IteratorValue(result). + // Step 4: IfAbruptRejectPromise(value, promiseCapability). + RootedValue value(cx); + if (!GetProperty(cx, resultObj, resultObj, cx->names().value, &value)) { + return AbruptRejectPromise(cx, args, resultPromise, nullptr); + } + + // Step numbers below include the changes in + // <https://github.com/tc39/ecma262/pull/1470>, which inserted a new step 6. + // + // Steps 7-9 (reordered). + // Step 7: Let steps be the algorithm steps defined in Async-from-Sync + // Iterator Value Unwrap Functions. + // Step 8: Let onFulfilled be CreateBuiltinFunction(steps, « [[Done]] »). + // Step 9: Set onFulfilled.[[Done]] to done. + PromiseHandler onFulfilled = + done ? PromiseHandler::AsyncFromSyncIteratorValueUnwrapDone + : PromiseHandler::AsyncFromSyncIteratorValueUnwrapNotDone; + PromiseHandler onRejected = PromiseHandler::Thrower; + + // Steps 5 and 10 are identical to some steps in Await; we have a utility + // function InternalAwait() that implements the idiom. + // + // Step 5: Let valueWrapper be PromiseResolve(%Promise%, « value »). + // Step 6: IfAbruptRejectPromise(valueWrapper, promiseCapability). + // Step 10: Perform ! PerformPromiseThen(valueWrapper, onFulfilled, + // undefined, promiseCapability). + auto extra = [](Handle<PromiseReactionRecord*> reaction) {}; + if (!InternalAwait(cx, value, resultPromise, onFulfilled, onRejected, + extra)) { + return AbruptRejectPromise(cx, args, resultPromise, nullptr); + } + + // Step 11: Return promiseCapability.[[Promise]]. + args.rval().setObject(*resultPromise); + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.prototype.catch ( onRejected ) + * https://tc39.es/ecma262/#sec-promise.prototype.catch + */ +static bool Promise_catch_impl(JSContext* cx, unsigned argc, Value* vp, + bool rvalExplicitlyUsed) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. Let promise be the this value. + HandleValue thisVal = args.thisv(); + HandleValue onFulfilled = UndefinedHandleValue; + HandleValue onRejected = args.get(0); + + // Fast path when the default Promise state is intact. + if (CanCallOriginalPromiseThenBuiltin(cx, thisVal)) { + return OriginalPromiseThenBuiltin(cx, thisVal, onFulfilled, onRejected, + args.rval(), rvalExplicitlyUsed); + } + + // Step 2. Return ? Invoke(promise, "then", « undefined, onRejected »). + RootedValue thenVal(cx); + if (!GetProperty(cx, thisVal, cx->names().then, &thenVal)) { + return false; + } + + if (IsNativeFunction(thenVal, &Promise_then) && + thenVal.toObject().nonCCWRealm() == cx->realm()) { + return Promise_then_impl(cx, thisVal, onFulfilled, onRejected, args.rval(), + rvalExplicitlyUsed); + } + + return Call(cx, thenVal, thisVal, UndefinedHandleValue, onRejected, + args.rval()); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.prototype.catch ( onRejected ) + * https://tc39.es/ecma262/#sec-promise.prototype.catch + */ +static bool Promise_catch_noRetVal(JSContext* cx, unsigned argc, Value* vp) { + return Promise_catch_impl(cx, argc, vp, false); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.prototype.catch ( onRejected ) + * https://tc39.es/ecma262/#sec-promise.prototype.catch + */ +static bool Promise_catch(JSContext* cx, unsigned argc, Value* vp) { + return Promise_catch_impl(cx, argc, vp, true); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.prototype.then ( onFulfilled, onRejected ) + * https://tc39.es/ecma262/#sec-promise.prototype.then + */ +static bool Promise_then_impl(JSContext* cx, HandleValue promiseVal, + HandleValue onFulfilled, HandleValue onRejected, + MutableHandleValue rval, + bool rvalExplicitlyUsed) { + // Step 1. Let promise be the this value. + // (implicit) + + // Step 2. If IsPromise(promise) is false, throw a TypeError exception. + if (!promiseVal.isObject()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_OBJECT_REQUIRED, + "Receiver of Promise.prototype.then call"); + return false; + } + + // Fast path when the default Promise state is intact. + if (CanCallOriginalPromiseThenBuiltin(cx, promiseVal)) { + // Steps 3-5. + return OriginalPromiseThenBuiltin(cx, promiseVal, onFulfilled, onRejected, + rval, rvalExplicitlyUsed); + } + + RootedObject promiseObj(cx, &promiseVal.toObject()); + Rooted<PromiseObject*> unwrappedPromise( + cx, + UnwrapAndTypeCheckValue<PromiseObject>(cx, promiseVal, [cx, &promiseVal] { + JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_PROTO, "Promise", "then", + InformalValueTypeName(promiseVal)); + })); + if (!unwrappedPromise) { + return false; + } + + bool rvalUsed = + rvalExplicitlyUsed || + IsPromiseThenOrCatchRetValImplicitlyUsed(cx, unwrappedPromise); + + // Step 3. Let C be ? SpeciesConstructor(promise, %Promise%). + // Step 4. Let resultCapability be ? NewPromiseCapability(C). + CreateDependentPromise createDependent = + rvalUsed ? CreateDependentPromise::Always + : CreateDependentPromise::SkipIfCtorUnobservable; + Rooted<PromiseCapability> resultCapability(cx); + if (!PromiseThenNewPromiseCapability(cx, promiseObj, createDependent, + &resultCapability)) { + return false; + } + + // Step 5. Return PerformPromiseThen(promise, onFulfilled, onRejected, + // resultCapability). + if (!PerformPromiseThen(cx, unwrappedPromise, onFulfilled, onRejected, + resultCapability)) { + return false; + } + + if (rvalUsed) { + rval.setObject(*resultCapability.promise()); + } else { + rval.setUndefined(); + } + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.prototype.then ( onFulfilled, onRejected ) + * https://tc39.es/ecma262/#sec-promise.prototype.then + */ +bool Promise_then_noRetVal(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return Promise_then_impl(cx, args.thisv(), args.get(0), args.get(1), + args.rval(), false); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * Promise.prototype.then ( onFulfilled, onRejected ) + * https://tc39.es/ecma262/#sec-promise.prototype.then + */ +bool js::Promise_then(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return Promise_then_impl(cx, args.thisv(), args.get(0), args.get(1), + args.rval(), true); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * PerformPromiseThen ( promise, onFulfilled, onRejected + * [ , resultCapability ] ) + * https://tc39.es/ecma262/#sec-performpromisethen + * + * Steps 1-12. + */ +[[nodiscard]] static bool PerformPromiseThen( + JSContext* cx, Handle<PromiseObject*> promise, HandleValue onFulfilled_, + HandleValue onRejected_, Handle<PromiseCapability> resultCapability) { + // Step 1. Assert: IsPromise(promise) is true. + // Step 2. If resultCapability is not present, then + // Step 2. a. Set resultCapability to undefined. + // (implicit) + + // (reordered) + // Step 4. Else, + // Step 4. a. Let onFulfilledJobCallback be HostMakeJobCallback(onFulfilled). + RootedValue onFulfilled(cx, onFulfilled_); + + // Step 3. If IsCallable(onFulfilled) is false, then + if (!IsCallable(onFulfilled)) { + // Step 3. a. Let onFulfilledJobCallback be empty. + onFulfilled = Int32Value(int32_t(PromiseHandler::Identity)); + } + + // (reordered) + // Step 6. Else, + // Step 6. a. Let onRejectedJobCallback be HostMakeJobCallback(onRejected). + RootedValue onRejected(cx, onRejected_); + + // Step 5. If IsCallable(onRejected) is false, then + if (!IsCallable(onRejected)) { + // Step 5. a. Let onRejectedJobCallback be empty. + onRejected = Int32Value(int32_t(PromiseHandler::Thrower)); + } + + // Step 7. Let fulfillReaction be the PromiseReaction + // { [[Capability]]: resultCapability, [[Type]]: Fulfill, + // [[Handler]]: onFulfilledJobCallback }. + // Step 8. Let rejectReaction be the PromiseReaction + // { [[Capability]]: resultCapability, [[Type]]: Reject, + // [[Handler]]: onRejectedJobCallback }. + // + // NOTE: We use single object for both reactions. + Rooted<PromiseReactionRecord*> reaction( + cx, NewReactionRecord(cx, resultCapability, onFulfilled, onRejected, + IncumbentGlobalObject::Yes)); + if (!reaction) { + return false; + } + + // Steps 9-14. + return PerformPromiseThenWithReaction(cx, promise, reaction); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * PerformPromiseThen ( promise, onFulfilled, onRejected + * [ , resultCapability ] ) + * https://tc39.es/ecma262/#sec-performpromisethen + */ +[[nodiscard]] static bool PerformPromiseThenWithoutSettleHandlers( + JSContext* cx, Handle<PromiseObject*> promise, + Handle<PromiseObject*> promiseToResolve, + Handle<PromiseCapability> resultCapability) { + // Step 1. Assert: IsPromise(promise) is true. + // Step 2. If resultCapability is not present, then + // (implicit) + + // Step 3. If IsCallable(onFulfilled) is false, then + // Step 3.a. Let onFulfilledJobCallback be empty. + HandleValue onFulfilled = NullHandleValue; + + // Step 5. If IsCallable(onRejected) is false, then + // Step 5.a. Let onRejectedJobCallback be empty. + HandleValue onRejected = NullHandleValue; + + // Step 7. Let fulfillReaction be the PromiseReaction + // { [[Capability]]: resultCapability, [[Type]]: Fulfill, + // [[Handler]]: onFulfilledJobCallback }. + // Step 8. Let rejectReaction be the PromiseReaction + // { [[Capability]]: resultCapability, [[Type]]: Reject, + // [[Handler]]: onRejectedJobCallback }. + Rooted<PromiseReactionRecord*> reaction( + cx, NewReactionRecord(cx, resultCapability, onFulfilled, onRejected, + IncumbentGlobalObject::Yes)); + if (!reaction) { + return false; + } + + reaction->setIsDefaultResolvingHandler(promiseToResolve); + + // Steps 9-12. + return PerformPromiseThenWithReaction(cx, promise, reaction); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * PerformPromiseThen ( promise, onFulfilled, onRejected + * [ , resultCapability ] ) + * https://tc39.github.io/ecma262/#sec-performpromisethen + * + * Steps 9-12. + */ +[[nodiscard]] static bool PerformPromiseThenWithReaction( + JSContext* cx, Handle<PromiseObject*> unwrappedPromise, + Handle<PromiseReactionRecord*> reaction) { + // Step 9. If promise.[[PromiseState]] is pending, then + JS::PromiseState state = unwrappedPromise->state(); + int32_t flags = unwrappedPromise->flags(); + if (state == JS::PromiseState::Pending) { + // Step 9.a. Append fulfillReaction as the last element of the List that is + // promise.[[PromiseFulfillReactions]]. + // Step 9.b. Append rejectReaction as the last element of the List that is + // promise.[[PromiseRejectReactions]]. + // + // Instead of creating separate reaction records for fulfillment and + // rejection, we create a combined record. All places we use the record + // can handle that. + if (!AddPromiseReaction(cx, unwrappedPromise, reaction)) { + return false; + } + } + + // Steps 10-11. + else { + // Step 11.a. Assert: The value of promise.[[PromiseState]] is rejected. + MOZ_ASSERT_IF(state != JS::PromiseState::Fulfilled, + state == JS::PromiseState::Rejected); + + // Step 10.a. Let value be promise.[[PromiseResult]]. + // Step 11.b. Let reason be promise.[[PromiseResult]]. + RootedValue valueOrReason(cx, unwrappedPromise->valueOrReason()); + + // We might be operating on a promise from another compartment. In that + // case, we need to wrap the result/reason value before using it. + if (!cx->compartment()->wrap(cx, &valueOrReason)) { + return false; + } + + // Step 11.c. If promise.[[PromiseIsHandled]] is false, + // perform HostPromiseRejectionTracker(promise, "handle"). + if (state == JS::PromiseState::Rejected && + !(flags & PROMISE_FLAG_HANDLED)) { + cx->runtime()->removeUnhandledRejectedPromise(cx, unwrappedPromise); + } + + // Step 10.b. Let fulfillJob be + // NewPromiseReactionJob(fulfillReaction, value). + // Step 10.c. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], + // fulfillJob.[[Realm]]). + // Step 11.d. Let rejectJob be + // NewPromiseReactionJob(rejectReaction, reason). + // Step 11.e. Perform HostEnqueuePromiseJob(rejectJob.[[Job]], + // rejectJob.[[Realm]]). + if (!EnqueuePromiseReactionJob(cx, reaction, valueOrReason, state)) { + return false; + } + } + + // Step 12. Set promise.[[PromiseIsHandled]] to true. + unwrappedPromise->setHandled(); + + return true; +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * PerformPromiseThen ( promise, onFulfilled, onRejected + * [ , resultCapability ] ) + * https://tc39.github.io/ecma262/#sec-performpromisethen + * + * Steps 9.a-b. + */ +[[nodiscard]] static bool AddPromiseReaction( + JSContext* cx, Handle<PromiseObject*> unwrappedPromise, + Handle<PromiseReactionRecord*> reaction) { + MOZ_RELEASE_ASSERT(reaction->is<PromiseReactionRecord>()); + RootedValue reactionVal(cx, ObjectValue(*reaction)); + + // The code that creates Promise reactions can handle wrapped Promises, + // unwrapping them as needed. That means that the `promise` and `reaction` + // objects we have here aren't necessarily from the same compartment. In + // order to store the reaction on the promise, we have to ensure that it + // is properly wrapped. + mozilla::Maybe<AutoRealm> ar; + if (unwrappedPromise->compartment() != cx->compartment()) { + ar.emplace(cx, unwrappedPromise); + if (!cx->compartment()->wrap(cx, &reactionVal)) { + return false; + } + } + Handle<PromiseObject*> promise = unwrappedPromise; + + // Step 9.a. Append fulfillReaction as the last element of the List that is + // promise.[[PromiseFulfillReactions]]. + // Step 9.b. Append rejectReaction as the last element of the List that is + // promise.[[PromiseRejectReactions]]. + RootedValue reactionsVal(cx, promise->reactions()); + + if (reactionsVal.isUndefined()) { + // If no reactions existed so far, just store the reaction record directly. + promise->setFixedSlot(PromiseSlot_ReactionsOrResult, reactionVal); + return true; + } + + RootedObject reactionsObj(cx, &reactionsVal.toObject()); + + // If only a single reaction exists, it's stored directly instead of in a + // list. In that case, `reactionsObj` might be a wrapper, which we can + // always safely unwrap. + if (IsProxy(reactionsObj)) { + reactionsObj = UncheckedUnwrap(reactionsObj); + if (JS_IsDeadWrapper(reactionsObj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEAD_OBJECT); + return false; + } + MOZ_RELEASE_ASSERT(reactionsObj->is<PromiseReactionRecord>()); + } + + if (reactionsObj->is<PromiseReactionRecord>()) { + // If a single reaction existed so far, create a list and store the + // old and the new reaction in it. + ArrayObject* reactions = NewDenseFullyAllocatedArray(cx, 2); + if (!reactions) { + return false; + } + + reactions->setDenseInitializedLength(2); + reactions->initDenseElement(0, reactionsVal); + reactions->initDenseElement(1, reactionVal); + + promise->setFixedSlot(PromiseSlot_ReactionsOrResult, + ObjectValue(*reactions)); + } else { + // Otherwise, just store the new reaction. + MOZ_RELEASE_ASSERT(reactionsObj->is<NativeObject>()); + Handle<NativeObject*> reactions = reactionsObj.as<NativeObject>(); + uint32_t len = reactions->getDenseInitializedLength(); + DenseElementResult result = reactions->ensureDenseElements(cx, len, 1); + if (result != DenseElementResult::Success) { + MOZ_ASSERT(result == DenseElementResult::Failure); + return false; + } + reactions->setDenseElement(len, reactionVal); + } + + return true; +} + +[[nodiscard]] static bool AddDummyPromiseReactionForDebugger( + JSContext* cx, Handle<PromiseObject*> promise, + HandleObject dependentPromise) { + if (promise->state() != JS::PromiseState::Pending) { + return true; + } + + // `dependentPromise` should be a maybe-wrapped Promise. + MOZ_ASSERT(UncheckedUnwrap(dependentPromise)->is<PromiseObject>()); + + // Leave resolve and reject as null. + Rooted<PromiseCapability> capability(cx); + capability.promise().set(dependentPromise); + + Rooted<PromiseReactionRecord*> reaction( + cx, NewReactionRecord(cx, capability, NullHandleValue, NullHandleValue, + IncumbentGlobalObject::No)); + if (!reaction) { + return false; + } + + reaction->setIsDebuggerDummy(); + + return AddPromiseReaction(cx, promise, reaction); +} + +uint64_t PromiseObject::getID() { return PromiseDebugInfo::id(this); } + +double PromiseObject::lifetime() { + return MillisecondsSinceStartup() - allocationTime(); +} + +/** + * Returns all promises that directly depend on this one. That means those + * created by calling `then` on this promise, or the promise returned by + * `Promise.all(iterable)` or `Promise.race(iterable)`, with this promise + * being a member of the passed-in `iterable`. + * + * Per spec, we should have separate lists of reaction records for the + * fulfill and reject cases. As an optimization, we have only one of those, + * containing the required data for both cases. So we just walk that list + * and extract the dependent promises from all reaction records. + */ +bool PromiseObject::dependentPromises(JSContext* cx, + MutableHandle<GCVector<Value>> values) { + if (state() != JS::PromiseState::Pending) { + return true; + } + + uint32_t valuesIndex = 0; + RootedValue reactionsVal(cx, reactions()); + + return ForEachReaction(cx, reactionsVal, [&](MutableHandleObject obj) { + if (IsProxy(obj)) { + obj.set(UncheckedUnwrap(obj)); + } + + if (JS_IsDeadWrapper(obj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEAD_OBJECT); + return false; + } + + MOZ_RELEASE_ASSERT(obj->is<PromiseReactionRecord>()); + Rooted<PromiseReactionRecord*> reaction(cx, + &obj->as<PromiseReactionRecord>()); + + // Not all reactions have a Promise on them. + RootedObject promiseObj(cx, reaction->promise()); + if (promiseObj) { + if (!values.growBy(1)) { + return false; + } + + values[valuesIndex++].setObject(*promiseObj); + } + return true; + }); +} + +bool PromiseObject::forEachReactionRecord( + JSContext* cx, PromiseReactionRecordBuilder& builder) { + if (state() != JS::PromiseState::Pending) { + // Promise was resolved, so no reaction records are present. + return true; + } + + RootedValue reactionsVal(cx, reactions()); + if (reactionsVal.isNullOrUndefined()) { + // No reaction records are attached to this promise. + return true; + } + + return ForEachReaction(cx, reactionsVal, [&](MutableHandleObject obj) { + if (IsProxy(obj)) { + obj.set(UncheckedUnwrap(obj)); + } + + if (JS_IsDeadWrapper(obj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEAD_OBJECT); + return false; + } + + Rooted<PromiseReactionRecord*> reaction(cx, + &obj->as<PromiseReactionRecord>()); + MOZ_ASSERT(reaction->targetState() == JS::PromiseState::Pending); + + if (reaction->isAsyncFunction()) { + Rooted<AsyncFunctionGeneratorObject*> generator( + cx, reaction->asyncFunctionGenerator()); + if (!builder.asyncFunction(cx, generator)) { + return false; + } + } else if (reaction->isAsyncGenerator()) { + Rooted<AsyncGeneratorObject*> generator(cx, reaction->asyncGenerator()); + if (!builder.asyncGenerator(cx, generator)) { + return false; + } + } else if (reaction->isDefaultResolvingHandler()) { + Rooted<PromiseObject*> promise(cx, reaction->defaultResolvingPromise()); + if (!builder.direct(cx, promise)) { + return false; + } + } else { + RootedObject resolve(cx); + RootedObject reject(cx); + RootedObject result(cx, reaction->promise()); + + Value v = reaction->getFixedSlot(ReactionRecordSlot_OnFulfilled); + if (v.isObject()) { + resolve = &v.toObject(); + } + + v = reaction->getFixedSlot(ReactionRecordSlot_OnRejected); + if (v.isObject()) { + reject = &v.toObject(); + } + + if (!builder.then(cx, resolve, reject, result)) { + return false; + } + } + + return true; + }); +} + +/** + * ES2023 draft rev 714fa3dd1e8237ae9c666146270f81880089eca5 + * + * Promise Reject Functions + * https://tc39.es/ecma262/#sec-promise-reject-functions + */ +static bool CallDefaultPromiseResolveFunction(JSContext* cx, + Handle<PromiseObject*> promise, + HandleValue resolutionValue) { + MOZ_ASSERT(IsPromiseWithDefaultResolvingFunction(promise)); + + // Steps 1-3. + // (implicit) + + // Step 4. Let alreadyResolved be F.[[AlreadyResolved]]. + // Step 5. If alreadyResolved.[[Value]] is true, return undefined. + if (IsAlreadyResolvedPromiseWithDefaultResolvingFunction(promise)) { + return true; + } + + // Step 6. Set alreadyResolved.[[Value]] to true. + SetAlreadyResolvedPromiseWithDefaultResolvingFunction(promise); + + // Steps 7-15. + // (implicit) Step 16. Return undefined. + return ResolvePromiseInternal(cx, promise, resolutionValue); +} + +/* static */ +bool PromiseObject::resolve(JSContext* cx, Handle<PromiseObject*> promise, + HandleValue resolutionValue) { + MOZ_ASSERT(!PromiseHasAnyFlag(*promise, PROMISE_FLAG_ASYNC)); + if (promise->state() != JS::PromiseState::Pending) { + return true; + } + + if (IsPromiseWithDefaultResolvingFunction(promise)) { + return CallDefaultPromiseResolveFunction(cx, promise, resolutionValue); + } + + JSFunction* resolveFun = GetResolveFunctionFromPromise(promise); + if (!resolveFun) { + return true; + } + + RootedValue funVal(cx, ObjectValue(*resolveFun)); + + // For xray'd Promises, the resolve fun may have been created in another + // compartment. For the call below to work in that case, wrap the + // function into the current compartment. + if (!cx->compartment()->wrap(cx, &funVal)) { + return false; + } + + RootedValue dummy(cx); + return Call(cx, funVal, UndefinedHandleValue, resolutionValue, &dummy); +} + +/** + * ES2023 draft rev 714fa3dd1e8237ae9c666146270f81880089eca5 + * + * Promise Reject Functions + * https://tc39.es/ecma262/#sec-promise-reject-functions + */ +static bool CallDefaultPromiseRejectFunction( + JSContext* cx, Handle<PromiseObject*> promise, HandleValue rejectionValue, + JS::Handle<SavedFrame*> unwrappedRejectionStack /* = nullptr */) { + MOZ_ASSERT(IsPromiseWithDefaultResolvingFunction(promise)); + + // Steps 1-3. + // (implicit) + + // Step 4. Let alreadyResolved be F.[[AlreadyResolved]]. + // Step 5. If alreadyResolved.[[Value]] is true, return undefined. + if (IsAlreadyResolvedPromiseWithDefaultResolvingFunction(promise)) { + return true; + } + + // Step 6. Set alreadyResolved.[[Value]] to true. + SetAlreadyResolvedPromiseWithDefaultResolvingFunction(promise); + + return RejectPromiseInternal(cx, promise, rejectionValue, + unwrappedRejectionStack); +} + +/* static */ +bool PromiseObject::reject(JSContext* cx, Handle<PromiseObject*> promise, + HandleValue rejectionValue) { + MOZ_ASSERT(!PromiseHasAnyFlag(*promise, PROMISE_FLAG_ASYNC)); + if (promise->state() != JS::PromiseState::Pending) { + return true; + } + + if (IsPromiseWithDefaultResolvingFunction(promise)) { + return CallDefaultPromiseRejectFunction(cx, promise, rejectionValue); + } + + RootedValue funVal(cx, promise->getFixedSlot(PromiseSlot_RejectFunction)); + MOZ_ASSERT(IsCallable(funVal)); + + RootedValue dummy(cx); + return Call(cx, funVal, UndefinedHandleValue, rejectionValue, &dummy); +} + +/** + * ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + * + * RejectPromise ( promise, reason ) + * https://tc39.es/ecma262/#sec-rejectpromise + * + * Step 7. + */ +/* static */ +void PromiseObject::onSettled(JSContext* cx, Handle<PromiseObject*> promise, + Handle<SavedFrame*> unwrappedRejectionStack) { + PromiseDebugInfo::setResolutionInfo(cx, promise, unwrappedRejectionStack); + + // Step 7. If promise.[[PromiseIsHandled]] is false, perform + // HostPromiseRejectionTracker(promise, "reject"). + if (promise->state() == JS::PromiseState::Rejected && + promise->isUnhandled()) { + cx->runtime()->addUnhandledRejectedPromise(cx, promise); + } + + DebugAPI::onPromiseSettled(cx, promise); +} + +void PromiseObject::setRequiresUserInteractionHandling(bool state) { + if (state) { + AddPromiseFlags(*this, PROMISE_FLAG_REQUIRES_USER_INTERACTION_HANDLING); + } else { + RemovePromiseFlags(*this, PROMISE_FLAG_REQUIRES_USER_INTERACTION_HANDLING); + } +} + +void PromiseObject::setHadUserInteractionUponCreation(bool state) { + if (state) { + AddPromiseFlags(*this, PROMISE_FLAG_HAD_USER_INTERACTION_UPON_CREATION); + } else { + RemovePromiseFlags(*this, PROMISE_FLAG_HAD_USER_INTERACTION_UPON_CREATION); + } +} + +void PromiseObject::copyUserInteractionFlagsFrom(PromiseObject& rhs) { + setRequiresUserInteractionHandling(rhs.requiresUserInteractionHandling()); + setHadUserInteractionUponCreation(rhs.hadUserInteractionUponCreation()); +} + +// We can skip `await` with an already resolved value only if the current frame +// is the topmost JS frame and the current job is the last job in the job queue. +// This guarantees that any new job enqueued in the current turn will be +// executed immediately after the current job. +// +// Currently we only support skipping jobs when the async function is resumed +// at least once. +[[nodiscard]] static bool IsTopMostAsyncFunctionCall(JSContext* cx) { + FrameIter iter(cx); + + // The current frame should be the async function. + if (iter.done()) { + return false; + } + + if (!iter.isFunctionFrame() && iter.isModuleFrame()) { + // The iterator is not a function frame, it is a module frame. + // Ignore this optimization for now. + return true; + } + + MOZ_ASSERT(iter.calleeTemplate()->isAsync()); + +#ifdef DEBUG + bool isGenerator = iter.calleeTemplate()->isGenerator(); +#endif + + ++iter; + + // The parent frame should be the `next` function of the generator that is + // internally called in AsyncFunctionResume resp. AsyncGeneratorResume. + if (iter.done()) { + return false; + } + // The initial call into an async function can happen from top-level code, so + // the parent frame isn't required to be a function frame. Contrary to that, + // the parent frame for an async generator function is always a function + // frame, because async generators can't directly fall through to an `await` + // expression from their initial call. + if (!iter.isFunctionFrame()) { + MOZ_ASSERT(!isGenerator); + return false; + } + + // Always skip InterpretGeneratorResume if present. + JSFunction* fun = iter.calleeTemplate(); + if (IsSelfHostedFunctionWithName(fun, cx->names().InterpretGeneratorResume)) { + ++iter; + + if (iter.done()) { + return false; + } + + MOZ_ASSERT(iter.isFunctionFrame()); + fun = iter.calleeTemplate(); + } + + if (!IsSelfHostedFunctionWithName(fun, cx->names().AsyncFunctionNext) && + !IsSelfHostedFunctionWithName(fun, cx->names().AsyncGeneratorNext)) { + return false; + } + + ++iter; + + // There should be no more frames. + if (iter.done()) { + return true; + } + + return false; +} + +[[nodiscard]] bool js::CanSkipAwait(JSContext* cx, HandleValue val, + bool* canSkip) { + if (!cx->canSkipEnqueuingJobs) { + *canSkip = false; + return true; + } + + if (!IsTopMostAsyncFunctionCall(cx)) { + *canSkip = false; + return true; + } + + // Primitive values cannot be 'thenables', so we can trivially skip the + // await operation. + if (!val.isObject()) { + *canSkip = true; + return true; + } + + JSObject* obj = &val.toObject(); + if (!obj->is<PromiseObject>()) { + *canSkip = false; + return true; + } + + PromiseObject* promise = &obj->as<PromiseObject>(); + + if (promise->state() == JS::PromiseState::Pending) { + *canSkip = false; + return true; + } + + PromiseLookup& promiseLookup = cx->realm()->promiseLookup; + if (!promiseLookup.isDefaultInstance(cx, promise)) { + *canSkip = false; + return true; + } + + if (promise->state() == JS::PromiseState::Rejected) { + // We don't optimize rejected Promises for now. + *canSkip = false; + return true; + } + + *canSkip = true; + return true; +} + +[[nodiscard]] bool js::ExtractAwaitValue(JSContext* cx, HandleValue val, + MutableHandleValue resolved) { +// Ensure all callers of this are jumping past the +// extract if it's not possible to extract. +#ifdef DEBUG + bool canSkip; + if (!CanSkipAwait(cx, val, &canSkip)) { + return false; + } + MOZ_ASSERT(canSkip == true); +#endif + + // Primitive values cannot be 'thenables', so we can trivially skip the + // await operation. + if (!val.isObject()) { + resolved.set(val); + return true; + } + + JSObject* obj = &val.toObject(); + PromiseObject* promise = &obj->as<PromiseObject>(); + resolved.set(promise->value()); + + return true; +} + +JS::AutoDebuggerJobQueueInterruption::AutoDebuggerJobQueueInterruption() + : cx(nullptr) {} + +JS::AutoDebuggerJobQueueInterruption::~AutoDebuggerJobQueueInterruption() { + MOZ_ASSERT_IF(initialized(), cx->jobQueue->empty()); +} + +bool JS::AutoDebuggerJobQueueInterruption::init(JSContext* cx) { + MOZ_ASSERT(cx->jobQueue); + this->cx = cx; + saved = cx->jobQueue->saveJobQueue(cx); + return !!saved; +} + +void JS::AutoDebuggerJobQueueInterruption::runJobs() { + JS::AutoSaveExceptionState ases(cx); + cx->jobQueue->runJobs(cx); +} + +const JSJitInfo promise_then_info = { + {(JSJitGetterOp)Promise_then_noRetVal}, + {0}, /* unused */ + {0}, /* unused */ + JSJitInfo::IgnoresReturnValueNative, + JSJitInfo::AliasEverything, + JSVAL_TYPE_UNDEFINED, +}; + +const JSJitInfo promise_catch_info = { + {(JSJitGetterOp)Promise_catch_noRetVal}, + {0}, /* unused */ + {0}, /* unused */ + JSJitInfo::IgnoresReturnValueNative, + JSJitInfo::AliasEverything, + JSVAL_TYPE_UNDEFINED, +}; + +static const JSFunctionSpec promise_methods[] = { + JS_FNINFO("then", js::Promise_then, &promise_then_info, 2, 0), + JS_FNINFO("catch", Promise_catch, &promise_catch_info, 1, 0), + JS_SELF_HOSTED_FN("finally", "Promise_finally", 1, 0), JS_FS_END}; + +static const JSPropertySpec promise_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Promise", JSPROP_READONLY), JS_PS_END}; + +static const JSFunctionSpec promise_static_methods[] = { + JS_FN("all", Promise_static_all, 1, 0), + JS_FN("allSettled", Promise_static_allSettled, 1, 0), + JS_FN("any", Promise_static_any, 1, 0), + JS_FN("race", Promise_static_race, 1, 0), + JS_FN("reject", Promise_reject, 1, 0), + JS_FN("resolve", js::Promise_static_resolve, 1, 0), + JS_FS_END}; + +static const JSPropertySpec promise_static_properties[] = { + JS_SYM_GET(species, js::Promise_static_species, 0), JS_PS_END}; + +static const ClassSpec PromiseObjectClassSpec = { + GenericCreateConstructor<PromiseConstructor, 1, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<PromiseObject>, + promise_static_methods, + promise_static_properties, + promise_methods, + promise_properties}; + +const JSClass PromiseObject::class_ = { + "Promise", + JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS) | + JSCLASS_HAS_CACHED_PROTO(JSProto_Promise) | + JSCLASS_HAS_XRAYED_CONSTRUCTOR, + JS_NULL_CLASS_OPS, &PromiseObjectClassSpec}; + +const JSClass PromiseObject::protoClass_ = { + "Promise.prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_Promise), + JS_NULL_CLASS_OPS, &PromiseObjectClassSpec}; diff --git a/js/src/builtin/Promise.h b/js/src/builtin/Promise.h new file mode 100644 index 0000000000..30b0cc862c --- /dev/null +++ b/js/src/builtin/Promise.h @@ -0,0 +1,267 @@ +/* -*- 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 builtin_Promise_h +#define builtin_Promise_h + +#include "jstypes.h" // JS_PUBLIC_API + +#include "js/RootingAPI.h" // JS::{,Mutable}Handle +#include "js/TypeDecls.h" // JS::HandleObjectVector + +struct JS_PUBLIC_API JSContext; +class JS_PUBLIC_API JSObject; + +namespace JS { +class CallArgs; +class Value; +} // namespace JS + +namespace js { + +class AsyncFunctionGeneratorObject; +class AsyncGeneratorObject; +class PromiseObject; +class SavedFrame; + +enum class CompletionKind : uint8_t; + +enum class PromiseHandler : uint32_t { + Identity = 0, + Thrower, + + // ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14 + // + // Await in async function + // https://tc39.es/ecma262/#await + // + // Step 3. fulfilledClosure Abstract Closure. + // Step 5. rejectedClosure Abstract Closure. + AsyncFunctionAwaitedFulfilled, + AsyncFunctionAwaitedRejected, + + // Await in async generator + // https://tc39.es/ecma262/#await + // + // Step 3. fulfilledClosure Abstract Closure. + // Step 5. rejectedClosure Abstract Closure. + AsyncGeneratorAwaitedFulfilled, + AsyncGeneratorAwaitedRejected, + + // AsyncGeneratorAwaitReturn ( generator ) + // https://tc39.es/ecma262/#sec-asyncgeneratorawaitreturn + // + // Step 7. fulfilledClosure Abstract Closure. + // Step 9. rejectedClosure Abstract Closure. + AsyncGeneratorAwaitReturnFulfilled, + AsyncGeneratorAwaitReturnRejected, + + // AsyncGeneratorUnwrapYieldResumption + // https://tc39.es/ecma262/#sec-asyncgeneratorunwrapyieldresumption + // + // Steps 3-5 for awaited.[[Type]] handling. + AsyncGeneratorYieldReturnAwaitedFulfilled, + AsyncGeneratorYieldReturnAwaitedRejected, + + // AsyncFromSyncIteratorContinuation ( result, promiseCapability ) + // https://tc39.es/ecma262/#sec-asyncfromsynciteratorcontinuation + // + // Steps 7. unwrap Abstract Closure. + AsyncFromSyncIteratorValueUnwrapDone, + AsyncFromSyncIteratorValueUnwrapNotDone, + + // One past the maximum allowed PromiseHandler value. + Limit +}; + +// Promise.prototype.then. +extern bool Promise_then(JSContext* cx, unsigned argc, JS::Value* vp); + +// Promise[Symbol.species]. +extern bool Promise_static_species(JSContext* cx, unsigned argc, JS::Value* vp); + +// Promise.resolve. +extern bool Promise_static_resolve(JSContext* cx, unsigned argc, JS::Value* vp); + +/** + * Unforgeable version of the JS builtin Promise.all. + * + * Takes a HandleValueVector of Promise objects and returns a promise that's + * resolved with an array of resolution values when all those promises have + * been resolved, or rejected with the rejection value of the first rejected + * promise. + * + * Asserts that all objects in the `promises` vector are, maybe wrapped, + * instances of `Promise` or a subclass of `Promise`. + */ +[[nodiscard]] JSObject* GetWaitForAllPromise(JSContext* cx, + JS::HandleObjectVector promises); + +/** + * Enqueues resolve/reject reactions in the given Promise's reactions lists + * as though by calling the original value of Promise.prototype.then, and + * without regard to any Promise subclassing used in `promiseObj` itself. + */ +[[nodiscard]] PromiseObject* OriginalPromiseThen( + JSContext* cx, JS::Handle<JSObject*> promiseObj, + JS::Handle<JSObject*> onFulfilled, JS::Handle<JSObject*> onRejected); + +enum class UnhandledRejectionBehavior { Ignore, Report }; + +/** + * React to[0] `unwrappedPromise` (which may not be from the current realm) as + * if by using a fresh promise created for the provided nullable fulfill/reject + * IsCallable objects. + * + * However, no dependent Promise will be created, and mucking with `Promise`, + * `Promise.prototype.then`, and `Promise[Symbol.species]` will not alter this + * function's behavior. + * + * If `unwrappedPromise` rejects and `onRejected_` is null, handling is + * determined by `behavior`. If `behavior == Report`, a fresh Promise will be + * constructed and rejected on the fly (and thus will be reported as unhandled). + * But if `behavior == Ignore`, the rejection is ignored and is not reported as + * unhandled. + * + * Note: Reactions pushed using this function contain a null `promise` field. + * That field is only ever used by devtools, which have to treat these reactions + * specially. + * + * 0. The sense of "react" here is the sense of the term as defined by Web IDL: + * https://heycam.github.io/webidl/#dfn-perform-steps-once-promise-is-settled + */ +[[nodiscard]] extern bool ReactToUnwrappedPromise( + JSContext* cx, JS::Handle<PromiseObject*> unwrappedPromise, + JS::Handle<JSObject*> onFulfilled_, JS::Handle<JSObject*> onRejected_, + UnhandledRejectionBehavior behavior); + +/** + * PromiseResolve ( C, x ) + * + * The abstract operation PromiseResolve, given a constructor and a value, + * returns a new promise resolved with that value. + */ +[[nodiscard]] JSObject* PromiseResolve(JSContext* cx, + JS::Handle<JSObject*> constructor, + JS::Handle<JS::Value> value); + +/** + * Reject |promise| with the value of the current pending exception. + * + * |promise| must be from the current realm. Callers must enter the realm of + * |promise| if they are not already in it. + */ +[[nodiscard]] bool RejectPromiseWithPendingError( + JSContext* cx, JS::Handle<PromiseObject*> promise); + +/** + * Create the promise object which will be used as the return value of an async + * function. + */ +[[nodiscard]] PromiseObject* CreatePromiseObjectForAsync(JSContext* cx); + +/** + * Returns true if the given object is a promise created by + * either CreatePromiseObjectForAsync function or async generator's method. + */ +[[nodiscard]] bool IsPromiseForAsyncFunctionOrGenerator(JSObject* promise); + +[[nodiscard]] bool AsyncFunctionReturned( + JSContext* cx, JS::Handle<PromiseObject*> resultPromise, + JS::Handle<JS::Value> value); + +[[nodiscard]] bool AsyncFunctionThrown(JSContext* cx, + JS::Handle<PromiseObject*> resultPromise, + JS::Handle<JS::Value> reason); + +// Start awaiting `value` in an async function (, but doesn't suspend the +// async function's execution!). Returns the async function's result promise. +[[nodiscard]] JSObject* AsyncFunctionAwait( + JSContext* cx, JS::Handle<AsyncFunctionGeneratorObject*> genObj, + JS::Handle<JS::Value> value); + +// If the await operation can be skipped and the resolution value for `val` can +// be acquired, stored the resolved value to `resolved` and `true` to +// `*canSkip`. Otherwise, stores `false` to `*canSkip`. +[[nodiscard]] bool CanSkipAwait(JSContext* cx, JS::Handle<JS::Value> val, + bool* canSkip); +[[nodiscard]] bool ExtractAwaitValue(JSContext* cx, JS::Handle<JS::Value> val, + JS::MutableHandle<JS::Value> resolved); + +bool AsyncFromSyncIteratorMethod(JSContext* cx, JS::CallArgs& args, + CompletionKind completionKind); + +// Callback for describing promise reaction records, for use with +// PromiseObject::getReactionRecords. +struct PromiseReactionRecordBuilder { + // A reaction record created by a call to 'then' or 'catch', with functions to + // call on resolution or rejection, and the promise that will be settled + // according to the result of calling them. + // + // Note that resolve, reject, and result may not be same-compartment with cx, + // or with the promise we're inspecting. This function presents the values + // exactly as they appear in the reaction record. They may also be wrapped or + // unwrapped. + // + // Some reaction records refer to internal resolution or rejection functions + // that are not naturally represented as debuggee JavaScript functions. In + // this case, resolve and reject may be nullptr. + [[nodiscard]] virtual bool then(JSContext* cx, JS::Handle<JSObject*> resolve, + JS::Handle<JSObject*> reject, + JS::Handle<JSObject*> result) = 0; + + // A reaction record created when one native promise is resolved to another. + // The 'promise' argument is the promise that will be settled in the same way + // the promise this reaction record is attached to is settled. + // + // Note that promise may not be same-compartment with cx. This function + // presents the promise exactly as it appears in the reaction record. + [[nodiscard]] virtual bool direct( + JSContext* cx, JS::Handle<PromiseObject*> unwrappedPromise) = 0; + + // A reaction record that resumes an asynchronous function suspended at an + // await expression. The 'generator' argument is the generator object + // representing the call. + // + // Note that generator may not be same-compartment with cx. This function + // presents the generator exactly as it appears in the reaction record. + [[nodiscard]] virtual bool asyncFunction( + JSContext* cx, + JS::Handle<AsyncFunctionGeneratorObject*> unwrappedGenerator) = 0; + + // A reaction record that resumes an asynchronous generator suspended at an + // await expression. The 'generator' argument is the generator object + // representing the call. + // + // Note that generator may not be same-compartment with cx. This function + // presents the generator exactly as it appears in the reaction record. + [[nodiscard]] virtual bool asyncGenerator( + JSContext* cx, JS::Handle<AsyncGeneratorObject*> unwrappedGenerator) = 0; +}; + +[[nodiscard]] PromiseObject* CreatePromiseObjectForAsyncGenerator( + JSContext* cx); + +[[nodiscard]] bool ResolvePromiseInternal(JSContext* cx, + JS::Handle<JSObject*> promise, + JS::Handle<JS::Value> resolutionVal); +[[nodiscard]] bool RejectPromiseInternal( + JSContext* cx, JS::Handle<PromiseObject*> promise, + JS::Handle<JS::Value> reason, + JS::Handle<SavedFrame*> unwrappedRejectionStack = nullptr); + +[[nodiscard]] bool InternalAsyncGeneratorAwait( + JSContext* cx, JS::Handle<AsyncGeneratorObject*> generator, + JS::Handle<JS::Value> value, PromiseHandler onFulfilled, + PromiseHandler onRejected); + +bool IsPromiseWithDefaultResolvingFunction(PromiseObject* promise); +void SetAlreadyResolvedPromiseWithDefaultResolvingFunction( + PromiseObject* promise); + +} // namespace js + +#endif // builtin_Promise_h diff --git a/js/src/builtin/Promise.js b/js/src/builtin/Promise.js new file mode 100644 index 0000000000..60613f8e5a --- /dev/null +++ b/js/src/builtin/Promise.js @@ -0,0 +1,78 @@ +/* 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/. */ + +// Promise.prototype.finally proposal, stage 3. +// Promise.prototype.finally ( onFinally ) +function Promise_finally(onFinally) { + // Step 1. + var promise = this; + + // Step 2. + if (!IsObject(promise)) { + ThrowTypeError(JSMSG_INCOMPATIBLE_PROTO, "Promise", "finally", "value"); + } + + // Step 3. + var C = SpeciesConstructor(promise, GetBuiltinConstructor("Promise")); + + // Step 4. + assert(IsConstructor(C), "SpeciesConstructor returns a constructor function"); + + // Steps 5-6. + var thenFinally, catchFinally; + if (!IsCallable(onFinally)) { + thenFinally = onFinally; + catchFinally = onFinally; + } else { + // ThenFinally Function. + // The parentheses prevent the infering of a function name. + // prettier-ignore + (thenFinally) = function(value) { + // Steps 1-2 (implicit). + + // Step 3. + var result = callContentFunction(onFinally, undefined); + + // Steps 4-5 (implicit). + + // Step 6. + var promise = PromiseResolve(C, result); + + // Step 7. + // FIXME: spec issue - "be equivalent to a function that" is not a defined spec term. + // https://github.com/tc39/ecma262/issues/933 + + // Step 8. + return callContentFunction(promise.then, promise, function() { + return value; + }); + }; + + // CatchFinally Function. + // prettier-ignore + (catchFinally) = function(reason) { + // Steps 1-2 (implicit). + + // Step 3. + var result = callContentFunction(onFinally, undefined); + + // Steps 4-5 (implicit). + + // Step 6. + var promise = PromiseResolve(C, result); + + // Step 7. + // FIXME: spec issue - "be equivalent to a function that" is not a defined spec term. + // https://github.com/tc39/ecma262/issues/933 + + // Step 8. + return callContentFunction(promise.then, promise, function() { + throw reason; + }); + }; + } + + // Step 7. + return callContentFunction(promise.then, promise, thenFinally, catchFinally); +} diff --git a/js/src/builtin/RecordObject.cpp b/js/src/builtin/RecordObject.cpp new file mode 100644 index 0000000000..9b2f73d08e --- /dev/null +++ b/js/src/builtin/RecordObject.cpp @@ -0,0 +1,77 @@ +/* -*- 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 "builtin/RecordObject.h" + +#include "jsapi.h" + +#include "vm/ObjectOperations.h" +#include "vm/RecordType.h" + +#include "vm/JSObject-inl.h" + +using namespace js; + +// Record and Record proposal section 9.2.1 + +RecordObject* RecordObject::create(JSContext* cx, Handle<RecordType*> record) { + RecordObject* rec = NewBuiltinClassInstance<RecordObject>(cx); + if (!rec) { + return nullptr; + } + rec->setFixedSlot(PrimitiveValueSlot, ExtendedPrimitiveValue(*record)); + return rec; +} + +RecordType* RecordObject::unbox() const { + return &getFixedSlot(PrimitiveValueSlot) + .toExtendedPrimitive() + .as<RecordType>(); +} + +bool RecordObject::maybeUnbox(JSObject* obj, MutableHandle<RecordType*> rrec) { + if (obj->is<RecordType>()) { + rrec.set(&obj->as<RecordType>()); + return true; + } + if (obj->is<RecordObject>()) { + rrec.set(obj->as<RecordObject>().unbox()); + return true; + } + return false; +} + +bool rec_resolve(JSContext* cx, HandleObject obj, HandleId id, + bool* resolvedp) { + RootedValue value(cx); + *resolvedp = obj->as<RecordObject>().unbox()->getOwnProperty(cx, id, &value); + + if (*resolvedp) { + static const unsigned RECORD_PROPERTY_ATTRS = + JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT; + return DefineDataProperty(cx, obj, id, value, + RECORD_PROPERTY_ATTRS | JSPROP_RESOLVING); + } + + return true; +} + +static const JSClassOps RecordObjectClassOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + rec_resolve, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass RecordObject::class_ = {"RecordObject", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount), + &RecordObjectClassOps}; diff --git a/js/src/builtin/RecordObject.h b/js/src/builtin/RecordObject.h new file mode 100644 index 0000000000..dee4d2738b --- /dev/null +++ b/js/src/builtin/RecordObject.h @@ -0,0 +1,31 @@ +/* -*- 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 builtin_RecordObject_h +#define builtin_RecordObject_h + +#include "vm/JSObject.h" +#include "vm/NativeObject.h" +#include "vm/RecordType.h" + +namespace js { + +class RecordObject : public NativeObject { + enum { PrimitiveValueSlot, SlotCount }; + + public: + static const JSClass class_; + + static RecordObject* create(JSContext* cx, Handle<RecordType*> record); + + JS::RecordType* unbox() const; + + static bool maybeUnbox(JSObject* obj, MutableHandle<RecordType*> rrec); +}; + +} // namespace js + +#endif diff --git a/js/src/builtin/Reflect.cpp b/js/src/builtin/Reflect.cpp new file mode 100644 index 0000000000..4c2e3f1742 --- /dev/null +++ b/js/src/builtin/Reflect.cpp @@ -0,0 +1,234 @@ +/* -*- 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 "builtin/Reflect.h" + +#include "jsapi.h" + +#include "builtin/Object.h" +#include "jit/InlinableNatives.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_NOT_EXPECTED_TYPE +#include "js/PropertySpec.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" + +#include "vm/GeckoProfiler-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/ObjectOperations-inl.h" + +using namespace js; + +/*** Reflect methods ********************************************************/ + +/* ES6 26.1.4 Reflect.deleteProperty (target, propertyKey) */ +static bool Reflect_deleteProperty(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject target( + cx, + RequireObjectArg(cx, "`target`", "Reflect.deleteProperty", args.get(0))); + if (!target) { + return false; + } + + // Steps 2-3. + RootedValue propertyKey(cx, args.get(1)); + RootedId key(cx); + if (!ToPropertyKey(cx, propertyKey, &key)) { + return false; + } + + // Step 4. + ObjectOpResult result; + if (!DeleteProperty(cx, target, key, result)) { + return false; + } + args.rval().setBoolean(result.ok()); + return true; +} + +/* ES6 26.1.8 Reflect.getPrototypeOf(target) */ +bool js::Reflect_getPrototypeOf(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject target( + cx, + RequireObjectArg(cx, "`target`", "Reflect.getPrototypeOf", args.get(0))); + if (!target) { + return false; + } + + // Step 2. + RootedObject proto(cx); + if (!GetPrototype(cx, target, &proto)) { + return false; + } + args.rval().setObjectOrNull(proto); + return true; +} + +/* ES6 draft 26.1.10 Reflect.isExtensible(target) */ +bool js::Reflect_isExtensible(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject target( + cx, + RequireObjectArg(cx, "`target`", "Reflect.isExtensible", args.get(0))); + if (!target) { + return false; + } + + // Step 2. + bool extensible; + if (!IsExtensible(cx, target, &extensible)) { + return false; + } + args.rval().setBoolean(extensible); + return true; +} + +// ES2018 draft rev c164be80f7ea91de5526b33d54e5c9321ed03d3f +// 26.1.10 Reflect.ownKeys ( target ) +bool js::Reflect_ownKeys(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "Reflect", "ownKeys"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject target( + cx, RequireObjectArg(cx, "`target`", "Reflect.ownKeys", args.get(0))); + if (!target) { + return false; + } + + // Steps 2-3. + return GetOwnPropertyKeys( + cx, target, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS, args.rval()); +} + +/* ES6 26.1.12 Reflect.preventExtensions(target) */ +static bool Reflect_preventExtensions(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject target( + cx, RequireObjectArg(cx, "`target`", "Reflect.preventExtensions", + args.get(0))); + if (!target) { + return false; + } + + // Step 2. + ObjectOpResult result; + if (!PreventExtensions(cx, target, result)) { + return false; + } + args.rval().setBoolean(result.ok()); + return true; +} + +/* ES6 26.1.13 Reflect.set(target, propertyKey, V [, receiver]) */ +static bool Reflect_set(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject target( + cx, RequireObjectArg(cx, "`target`", "Reflect.set", args.get(0))); + if (!target) { + return false; + } + + // Steps 2-3. + RootedValue propertyKey(cx, args.get(1)); + RootedId key(cx); + if (!ToPropertyKey(cx, propertyKey, &key)) { + return false; + } + + // Step 4. + RootedValue receiver(cx, args.length() > 3 ? args[3] : args.get(0)); + + // Step 5. + ObjectOpResult result; + RootedValue value(cx, args.get(2)); + if (!SetProperty(cx, target, key, value, receiver, result)) { + return false; + } + args.rval().setBoolean(result.ok()); + return true; +} + +/* + * ES6 26.1.3 Reflect.setPrototypeOf(target, proto) + * + * The specification is not quite similar enough to Object.setPrototypeOf to + * share code. + */ +static bool Reflect_setPrototypeOf(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedObject obj(cx, RequireObjectArg(cx, "`target`", + "Reflect.setPrototypeOf", args.get(0))); + if (!obj) { + return false; + } + + // Step 2. + if (!args.get(1).isObjectOrNull()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, "Reflect.setPrototypeOf", + "an object or null", + InformalValueTypeName(args.get(1))); + return false; + } + RootedObject proto(cx, args.get(1).toObjectOrNull()); + + // Step 4. + ObjectOpResult result; + if (!SetPrototype(cx, obj, proto, result)) { + return false; + } + args.rval().setBoolean(result.ok()); + return true; +} + +static const JSFunctionSpec reflect_methods[] = { + JS_SELF_HOSTED_FN("apply", "Reflect_apply", 3, 0), + JS_SELF_HOSTED_FN("construct", "Reflect_construct", 2, 0), + JS_SELF_HOSTED_FN("defineProperty", "Reflect_defineProperty", 3, 0), + JS_FN("deleteProperty", Reflect_deleteProperty, 2, 0), + JS_SELF_HOSTED_FN("get", "Reflect_get", 2, 0), + JS_SELF_HOSTED_FN("getOwnPropertyDescriptor", + "Reflect_getOwnPropertyDescriptor", 2, 0), + JS_INLINABLE_FN("getPrototypeOf", Reflect_getPrototypeOf, 1, 0, + ReflectGetPrototypeOf), + JS_SELF_HOSTED_FN("has", "Reflect_has", 2, 0), + JS_FN("isExtensible", Reflect_isExtensible, 1, 0), + JS_FN("ownKeys", Reflect_ownKeys, 1, 0), + JS_FN("preventExtensions", Reflect_preventExtensions, 1, 0), + JS_FN("set", Reflect_set, 3, 0), + JS_FN("setPrototypeOf", Reflect_setPrototypeOf, 2, 0), + JS_FS_END}; + +static const JSPropertySpec reflect_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Reflect", JSPROP_READONLY), JS_PS_END}; + +/*** Setup ******************************************************************/ + +static JSObject* CreateReflectObject(JSContext* cx, JSProtoKey key) { + RootedObject proto(cx, &cx->global()->getObjectPrototype()); + return NewPlainObjectWithProto(cx, proto, TenuredObject); +} + +static const ClassSpec ReflectClassSpec = {CreateReflectObject, nullptr, + reflect_methods, reflect_properties}; + +const JSClass js::ReflectClass = {"Reflect", 0, JS_NULL_CLASS_OPS, + &ReflectClassSpec}; diff --git a/js/src/builtin/Reflect.h b/js/src/builtin/Reflect.h new file mode 100644 index 0000000000..58dfa7a78b --- /dev/null +++ b/js/src/builtin/Reflect.h @@ -0,0 +1,33 @@ +/* -*- 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 builtin_Reflect_h +#define builtin_Reflect_h + +#include "js/Class.h" + +struct JSContext; + +namespace JS { +class Value; +} + +namespace js { + +extern const JSClass ReflectClass; + +[[nodiscard]] extern bool Reflect_getPrototypeOf(JSContext* cx, unsigned argc, + JS::Value* vp); + +[[nodiscard]] extern bool Reflect_isExtensible(JSContext* cx, unsigned argc, + JS::Value* vp); + +[[nodiscard]] extern bool Reflect_ownKeys(JSContext* cx, unsigned argc, + JS::Value* vp); + +} // namespace js + +#endif /* builtin_Reflect_h */ diff --git a/js/src/builtin/Reflect.js b/js/src/builtin/Reflect.js new file mode 100644 index 0000000000..f56d603ca3 --- /dev/null +++ b/js/src/builtin/Reflect.js @@ -0,0 +1,182 @@ +/* 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/. */ + +// ES2017 draft rev a785b0832b071f505a694e1946182adeab84c972 +// 7.3.17 CreateListFromArrayLike (obj [ , elementTypes ] ) +function CreateListFromArrayLikeForArgs(obj) { + // Step 1 (not applicable). + + // Step 2. + assert( + IsObject(obj), + "object must be passed to CreateListFromArrayLikeForArgs" + ); + + // Step 3. + var len = ToLength(obj.length); + + // This version of CreateListFromArrayLike is only used for argument lists. + if (len > MAX_ARGS_LENGTH) { + ThrowRangeError(JSMSG_TOO_MANY_ARGUMENTS); + } + + // Steps 4-6. + var list = std_Array(len); + for (var i = 0; i < len; i++) { + DefineDataProperty(list, i, obj[i]); + } + + // Step 7. + return list; +} + +// ES2017 draft rev a785b0832b071f505a694e1946182adeab84c972 +// 26.1.1 Reflect.apply ( target, thisArgument, argumentsList ) +function Reflect_apply(target, thisArgument, argumentsList) { + // Step 1. + if (!IsCallable(target)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, target)); + } + + // Step 2. + if (!IsObject(argumentsList)) { + ThrowTypeError( + JSMSG_OBJECT_REQUIRED_ARG, + "`argumentsList`", + "Reflect.apply", + ToSource(argumentsList) + ); + } + + // Steps 2-4. + return callFunction(std_Function_apply, target, thisArgument, argumentsList); +} + +// ES2017 draft rev a785b0832b071f505a694e1946182adeab84c972 +// 26.1.2 Reflect.construct ( target, argumentsList [ , newTarget ] ) +function Reflect_construct(target, argumentsList /*, newTarget*/) { + // Step 1. + if (!IsConstructor(target)) { + ThrowTypeError(JSMSG_NOT_CONSTRUCTOR, DecompileArg(0, target)); + } + + // Steps 2-3. + var newTarget; + if (ArgumentsLength() > 2) { + newTarget = GetArgument(2); + if (!IsConstructor(newTarget)) { + ThrowTypeError(JSMSG_NOT_CONSTRUCTOR, DecompileArg(2, newTarget)); + } + } else { + newTarget = target; + } + + // Step 4. + if (!IsObject(argumentsList)) { + ThrowTypeError( + JSMSG_OBJECT_REQUIRED_ARG, + "`argumentsList`", + "Reflect.construct", + ToSource(argumentsList) + ); + } + + // Fast path when we can avoid calling CreateListFromArrayLikeForArgs(). + var args = + IsPackedArray(argumentsList) && argumentsList.length <= MAX_ARGS_LENGTH + ? argumentsList + : CreateListFromArrayLikeForArgs(argumentsList); + + // Step 5. + switch (args.length) { + case 0: + return constructContentFunction(target, newTarget); + case 1: + return constructContentFunction(target, newTarget, SPREAD(args, 1)); + case 2: + return constructContentFunction(target, newTarget, SPREAD(args, 2)); + case 3: + return constructContentFunction(target, newTarget, SPREAD(args, 3)); + case 4: + return constructContentFunction(target, newTarget, SPREAD(args, 4)); + case 5: + return constructContentFunction(target, newTarget, SPREAD(args, 5)); + case 6: + return constructContentFunction(target, newTarget, SPREAD(args, 6)); + case 7: + return constructContentFunction(target, newTarget, SPREAD(args, 7)); + case 8: + return constructContentFunction(target, newTarget, SPREAD(args, 8)); + case 9: + return constructContentFunction(target, newTarget, SPREAD(args, 9)); + case 10: + return constructContentFunction(target, newTarget, SPREAD(args, 10)); + case 11: + return constructContentFunction(target, newTarget, SPREAD(args, 11)); + case 12: + return constructContentFunction(target, newTarget, SPREAD(args, 12)); + default: + return ConstructFunction(target, newTarget, args); + } +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 26.1.3 Reflect.defineProperty ( target, propertyKey, attributes ) +function Reflect_defineProperty(obj, propertyKey, attributes) { + // Steps 1-4. + return ObjectOrReflectDefineProperty(obj, propertyKey, attributes, false); +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 26.1.6 Reflect.getOwnPropertyDescriptor ( target, propertyKey ) +function Reflect_getOwnPropertyDescriptor(target, propertyKey) { + // Step 1. + if (!IsObject(target)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, DecompileArg(0, target)); + } + + // Steps 2-3. + // The other steps are identical to Object.getOwnPropertyDescriptor(). + return ObjectGetOwnPropertyDescriptor(target, propertyKey); +} + +// ES2017 draft rev a785b0832b071f505a694e1946182adeab84c972 +// 26.1.8 Reflect.has ( target, propertyKey ) +function Reflect_has(target, propertyKey) { + // Step 1. + if (!IsObject(target)) { + ThrowTypeError( + JSMSG_OBJECT_REQUIRED_ARG, + "`target`", + "Reflect.has", + ToSource(target) + ); + } + + // Steps 2-3 are identical to the runtime semantics of the "in" operator. + return propertyKey in target; +} + +// ES2018 draft rev 0525bb33861c7f4e9850f8a222c89642947c4b9c +// 26.1.5 Reflect.get ( target, propertyKey [ , receiver ] ) +function Reflect_get(target, propertyKey /*, receiver*/) { + // Step 1. + if (!IsObject(target)) { + ThrowTypeError( + JSMSG_OBJECT_REQUIRED_ARG, + "`target`", + "Reflect.get", + ToSource(target) + ); + } + + // Step 3 (reordered). + if (ArgumentsLength() > 2) { + // Steps 2, 4. + return getPropertySuper(target, propertyKey, GetArgument(2)); + } + + // Steps 2, 4. + return target[propertyKey]; +} diff --git a/js/src/builtin/ReflectParse.cpp b/js/src/builtin/ReflectParse.cpp new file mode 100644 index 0000000000..c3a0e1afc0 --- /dev/null +++ b/js/src/builtin/ReflectParse.cpp @@ -0,0 +1,3801 @@ +/* -*- 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/. */ + +/* JS reflection package. */ + +#include "mozilla/DebugOnly.h" + +#include <stdlib.h> +#include <utility> + +#include "jspubtd.h" + +#include "builtin/Array.h" +#include "frontend/CompilationStencil.h" +#include "frontend/FrontendContext.h" // AutoReportFrontendContext +#include "frontend/ModuleSharedContext.h" +#include "frontend/ParseNode.h" +#include "frontend/Parser.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/friend/StackLimits.h" // js::AutoCheckRecursionLimit +#include "js/PropertyAndElement.h" // JS_DefineFunction +#include "js/StableStringChars.h" +#include "vm/FunctionFlags.h" // js::FunctionFlags +#include "vm/Interpreter.h" +#include "vm/JSAtom.h" +#include "vm/JSObject.h" +#include "vm/ModuleBuilder.h" // js::ModuleBuilder +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/RegExpObject.h" + +#include "vm/JSAtom-inl.h" +#include "vm/JSContext-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/ObjectOperations-inl.h" + +using namespace js; +using namespace js::frontend; + +using JS::AutoStableStringChars; +using JS::CompileOptions; +using JS::RootedValueArray; +using mozilla::DebugOnly; + +enum ASTType { + AST_ERROR = -1, +#define ASTDEF(ast, str) ast, +#include "jsast.tbl" +#undef ASTDEF + AST_LIMIT +}; + +enum AssignmentOperator { + AOP_ERR = -1, + + /* assign */ + AOP_ASSIGN = 0, + /* operator-assign */ + AOP_PLUS, + AOP_MINUS, + AOP_STAR, + AOP_DIV, + AOP_MOD, + AOP_POW, + /* shift-assign */ + AOP_LSH, + AOP_RSH, + AOP_URSH, + /* binary */ + AOP_BITOR, + AOP_BITXOR, + AOP_BITAND, + /* short-circuit */ + AOP_COALESCE, + AOP_OR, + AOP_AND, + + AOP_LIMIT +}; + +enum BinaryOperator { + BINOP_ERR = -1, + + /* eq */ + BINOP_EQ = 0, + BINOP_NE, + BINOP_STRICTEQ, + BINOP_STRICTNE, + /* rel */ + BINOP_LT, + BINOP_LE, + BINOP_GT, + BINOP_GE, + /* shift */ + BINOP_LSH, + BINOP_RSH, + BINOP_URSH, + /* arithmetic */ + BINOP_ADD, + BINOP_SUB, + BINOP_STAR, + BINOP_DIV, + BINOP_MOD, + BINOP_POW, + /* binary */ + BINOP_BITOR, + BINOP_BITXOR, + BINOP_BITAND, + /* misc */ + BINOP_IN, + BINOP_INSTANCEOF, + BINOP_COALESCE, + + BINOP_LIMIT +}; + +enum UnaryOperator { + UNOP_ERR = -1, + + UNOP_DELETE = 0, + UNOP_NEG, + UNOP_POS, + UNOP_NOT, + UNOP_BITNOT, + UNOP_TYPEOF, + UNOP_VOID, + UNOP_AWAIT, + + UNOP_LIMIT +}; + +enum VarDeclKind { + VARDECL_ERR = -1, + VARDECL_VAR = 0, + VARDECL_CONST, + VARDECL_LET, + VARDECL_LIMIT +}; + +enum PropKind { + PROP_ERR = -1, + PROP_INIT = 0, + PROP_GETTER, + PROP_SETTER, + PROP_MUTATEPROTO, + PROP_LIMIT +}; + +static const char* const aopNames[] = { + "=", /* AOP_ASSIGN */ + "+=", /* AOP_PLUS */ + "-=", /* AOP_MINUS */ + "*=", /* AOP_STAR */ + "/=", /* AOP_DIV */ + "%=", /* AOP_MOD */ + "**=", /* AOP_POW */ + "<<=", /* AOP_LSH */ + ">>=", /* AOP_RSH */ + ">>>=", /* AOP_URSH */ + "|=", /* AOP_BITOR */ + "^=", /* AOP_BITXOR */ + "&=", /* AOP_BITAND */ + "\?\?=", /* AOP_COALESCE */ + "||=", /* AOP_OR */ + "&&=", /* AOP_AND */ +}; + +static const char* const binopNames[] = { + "==", /* BINOP_EQ */ + "!=", /* BINOP_NE */ + "===", /* BINOP_STRICTEQ */ + "!==", /* BINOP_STRICTNE */ + "<", /* BINOP_LT */ + "<=", /* BINOP_LE */ + ">", /* BINOP_GT */ + ">=", /* BINOP_GE */ + "<<", /* BINOP_LSH */ + ">>", /* BINOP_RSH */ + ">>>", /* BINOP_URSH */ + "+", /* BINOP_PLUS */ + "-", /* BINOP_MINUS */ + "*", /* BINOP_STAR */ + "/", /* BINOP_DIV */ + "%", /* BINOP_MOD */ + "**", /* BINOP_POW */ + "|", /* BINOP_BITOR */ + "^", /* BINOP_BITXOR */ + "&", /* BINOP_BITAND */ + "in", /* BINOP_IN */ + "instanceof", /* BINOP_INSTANCEOF */ + "??", /* BINOP_COALESCE */ +}; + +static const char* const unopNames[] = { + "delete", /* UNOP_DELETE */ + "-", /* UNOP_NEG */ + "+", /* UNOP_POS */ + "!", /* UNOP_NOT */ + "~", /* UNOP_BITNOT */ + "typeof", /* UNOP_TYPEOF */ + "void", /* UNOP_VOID */ + "await", /* UNOP_AWAIT */ +}; + +static const char* const nodeTypeNames[] = { +#define ASTDEF(ast, str) str, +#include "jsast.tbl" +#undef ASTDEF + nullptr}; + +enum YieldKind { Delegating, NotDelegating }; + +using NodeVector = RootedValueVector; + +/* + * ParseNode is a somewhat intricate data structure, and its invariants have + * evolved, making it more likely that there could be a disconnect between the + * parser and the AST serializer. We use these macros to check invariants on a + * parse node and raise a dynamic error on failure. + */ +#define LOCAL_ASSERT(expr) \ + JS_BEGIN_MACRO \ + MOZ_ASSERT(expr); \ + if (!(expr)) { \ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, \ + JSMSG_BAD_PARSE_NODE); \ + return false; \ + } \ + JS_END_MACRO + +#define LOCAL_NOT_REACHED(expr) \ + JS_BEGIN_MACRO \ + MOZ_ASSERT(false); \ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, \ + JSMSG_BAD_PARSE_NODE); \ + return false; \ + JS_END_MACRO + +namespace { + +/* Set 'result' to obj[id] if any such property exists, else defaultValue. */ +static bool GetPropertyDefault(JSContext* cx, HandleObject obj, HandleId id, + HandleValue defaultValue, + MutableHandleValue result) { + bool found; + if (!HasProperty(cx, obj, id, &found)) { + return false; + } + if (!found) { + result.set(defaultValue); + return true; + } + return GetProperty(cx, obj, obj, id, result); +} + +enum class GeneratorStyle { None, ES6 }; + +/* + * Builder class that constructs JavaScript AST node objects. + */ +class NodeBuilder { + using CallbackArray = RootedValueArray<AST_LIMIT>; + + JSContext* cx; + FrontendContext* fc; + frontend::Parser<frontend::FullParseHandler, char16_t>* parser; + bool saveLoc; /* save source location information? */ + char const* src; /* UTF-8 encoded source filename or null */ + RootedValue srcval; /* source filename JS value or null */ + + public: + NodeBuilder(JSContext* c, FrontendContext* f, bool l, char const* s) + : cx(c), fc(f), parser(nullptr), saveLoc(l), src(s), srcval(c) {} + + [[nodiscard]] bool init() { + if (src) { + if (!atomValueUtf8(src, &srcval)) { + return false; + } + } else { + srcval.setNull(); + } + + return true; + } + + void setParser(frontend::Parser<frontend::FullParseHandler, char16_t>* p) { + parser = p; + } + + private: + [[nodiscard]] bool atomValue(const char* s, MutableHandleValue dst) { + MOZ_ASSERT(JS::StringIsASCII(s)); + + /* + * Bug 575416: instead of Atomize, lookup constant atoms in tbl file + */ + Rooted<JSAtom*> atom(cx, Atomize(cx, s, strlen(s))); + if (!atom) { + return false; + } + + dst.setString(atom); + return true; + } + + [[nodiscard]] bool atomValueUtf8(const char* s, MutableHandleValue dst) { + Rooted<JSAtom*> atom(cx, AtomizeUTF8Chars(cx, s, strlen(s))); + if (!atom) { + return false; + } + + dst.setString(atom); + return true; + } + + [[nodiscard]] bool newObject(MutableHandleObject dst) { + Rooted<PlainObject*> nobj(cx, NewPlainObject(cx)); + if (!nobj) { + return false; + } + + dst.set(nobj); + return true; + } + + [[nodiscard]] bool newArray(NodeVector& elts, MutableHandleValue dst); + + [[nodiscard]] bool createNode(ASTType type, TokenPos* pos, + MutableHandleObject dst); + + [[nodiscard]] bool newNodeHelper(HandleObject obj, MutableHandleValue dst) { + // The end of the implementation of newNode(). + MOZ_ASSERT(obj); + dst.setObject(*obj); + return true; + } + + template <typename... Arguments> + [[nodiscard]] bool newNodeHelper(HandleObject obj, const char* name, + HandleValue value, Arguments&&... rest) { + // Recursive loop to define properties. Note that the newNodeHelper() + // call below passes two fewer arguments than we received, as we omit + // `name` and `value`. This eventually bottoms out in a call to the + // non-template newNodeHelper() above. + return defineProperty(obj, name, value) && + newNodeHelper(obj, std::forward<Arguments>(rest)...); + } + + // Create a node object with "type" and "loc" properties, as well as zero + // or more properties passed in as arguments. The signature is really more + // like: + // + // bool newNode(ASTType type, TokenPos* pos, + // {const char *name0, HandleValue value0,}... + // MutableHandleValue dst); + template <typename... Arguments> + [[nodiscard]] bool newNode(ASTType type, TokenPos* pos, Arguments&&... args) { + RootedObject node(cx); + return createNode(type, pos, &node) && + newNodeHelper(node, std::forward<Arguments>(args)...); + } + + [[nodiscard]] bool listNode(ASTType type, const char* propName, + NodeVector& elts, TokenPos* pos, + MutableHandleValue dst) { + RootedValue array(cx); + if (!newArray(elts, &array)) { + return false; + } + + return newNode(type, pos, propName, array, dst); + } + + [[nodiscard]] bool defineProperty(HandleObject obj, const char* name, + HandleValue val) { + MOZ_ASSERT_IF(val.isMagic(), val.whyMagic() == JS_SERIALIZE_NO_NODE); + + /* + * Bug 575416: instead of Atomize, lookup constant atoms in tbl file + */ + Rooted<JSAtom*> atom(cx, Atomize(cx, name, strlen(name))); + if (!atom) { + return false; + } + + // Represent "no node" as null and ensure users are not exposed to magic + // values. + RootedValue optVal(cx, + val.isMagic(JS_SERIALIZE_NO_NODE) ? NullValue() : val); + return DefineDataProperty(cx, obj, atom->asPropertyName(), optVal); + } + + [[nodiscard]] bool newNodeLoc(TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool setNodeLoc(HandleObject node, TokenPos* pos); + + public: + /* + * All of the public builder methods take as their last two + * arguments a nullable token position and a non-nullable, rooted + * outparam. + * + * Any Value arguments representing optional subnodes may be a + * JS_SERIALIZE_NO_NODE magic value. + */ + + /* + * misc nodes + */ + + [[nodiscard]] bool program(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool literal(HandleValue val, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool identifier(HandleValue name, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool function(ASTType type, TokenPos* pos, HandleValue id, + NodeVector& args, NodeVector& defaults, + HandleValue body, HandleValue rest, + GeneratorStyle generatorStyle, bool isAsync, + bool isExpression, MutableHandleValue dst); + + [[nodiscard]] bool variableDeclarator(HandleValue id, HandleValue init, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool switchCase(HandleValue expr, NodeVector& elts, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool catchClause(HandleValue var, HandleValue body, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool prototypeMutation(HandleValue val, TokenPos* pos, + MutableHandleValue dst); + [[nodiscard]] bool propertyInitializer(HandleValue key, HandleValue val, + PropKind kind, bool isShorthand, + bool isMethod, TokenPos* pos, + MutableHandleValue dst); + + /* + * statements + */ + + [[nodiscard]] bool blockStatement(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool expressionStatement(HandleValue expr, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool emptyStatement(TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool ifStatement(HandleValue test, HandleValue cons, + HandleValue alt, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool breakStatement(HandleValue label, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool continueStatement(HandleValue label, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool labeledStatement(HandleValue label, HandleValue stmt, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool throwStatement(HandleValue arg, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool returnStatement(HandleValue arg, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool forStatement(HandleValue init, HandleValue test, + HandleValue update, HandleValue stmt, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool forInStatement(HandleValue var, HandleValue expr, + HandleValue stmt, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool forOfStatement(HandleValue var, HandleValue expr, + HandleValue stmt, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool withStatement(HandleValue expr, HandleValue stmt, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool whileStatement(HandleValue test, HandleValue stmt, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool doWhileStatement(HandleValue stmt, HandleValue test, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool switchStatement(HandleValue disc, NodeVector& elts, + bool lexical, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool tryStatement(HandleValue body, HandleValue handler, + HandleValue finally, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool debuggerStatement(TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool moduleRequest(HandleValue moduleSpec, + NodeVector& assertions, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool importAssertion(HandleValue key, HandleValue value, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool importDeclaration(NodeVector& elts, HandleValue moduleSpec, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool importSpecifier(HandleValue importName, + HandleValue bindingName, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool importNamespaceSpecifier(HandleValue bindingName, + TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool exportDeclaration(HandleValue decl, NodeVector& elts, + HandleValue moduleSpec, + HandleValue isDefault, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool exportSpecifier(HandleValue bindingName, + HandleValue exportName, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool exportNamespaceSpecifier(HandleValue exportName, + TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool exportBatchSpecifier(TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool classDefinition(bool expr, HandleValue name, + HandleValue heritage, HandleValue block, + TokenPos* pos, MutableHandleValue dst); + [[nodiscard]] bool classMembers(NodeVector& members, MutableHandleValue dst); + [[nodiscard]] bool classMethod(HandleValue name, HandleValue body, + PropKind kind, bool isStatic, TokenPos* pos, + MutableHandleValue dst); + [[nodiscard]] bool classField(HandleValue name, HandleValue initializer, + TokenPos* pos, MutableHandleValue dst); + [[nodiscard]] bool staticClassBlock(HandleValue body, TokenPos* pos, + MutableHandleValue dst); + + /* + * expressions + */ + + [[nodiscard]] bool binaryExpression(BinaryOperator op, HandleValue left, + HandleValue right, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool unaryExpression(UnaryOperator op, HandleValue expr, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool assignmentExpression(AssignmentOperator op, + HandleValue lhs, HandleValue rhs, + TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool updateExpression(HandleValue expr, bool incr, bool prefix, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool logicalExpression(ParseNodeKind pnk, HandleValue left, + HandleValue right, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool conditionalExpression(HandleValue test, HandleValue cons, + HandleValue alt, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool sequenceExpression(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool newExpression(HandleValue callee, NodeVector& args, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool callExpression(HandleValue callee, NodeVector& args, + TokenPos* pos, MutableHandleValue dst, + bool isOptional = false); + + [[nodiscard]] bool memberExpression(bool computed, HandleValue expr, + HandleValue member, TokenPos* pos, + MutableHandleValue dst, + bool isOptional = false); + + [[nodiscard]] bool arrayExpression(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool templateLiteral(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool taggedTemplate(HandleValue callee, NodeVector& args, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool callSiteObj(NodeVector& raw, NodeVector& cooked, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool spreadExpression(HandleValue expr, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool optionalExpression(HandleValue expr, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool deleteOptionalExpression(HandleValue expr, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool computedName(HandleValue name, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool objectExpression(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool thisExpression(TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool yieldExpression(HandleValue arg, YieldKind kind, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool metaProperty(HandleValue meta, HandleValue property, + TokenPos* pos, MutableHandleValue dst); + + [[nodiscard]] bool callImportExpression(HandleValue ident, NodeVector& args, + TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool super(TokenPos* pos, MutableHandleValue dst); + +#ifdef ENABLE_RECORD_TUPLE + [[nodiscard]] bool recordExpression(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool tupleExpression(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst); +#endif + + /* + * declarations + */ + + [[nodiscard]] bool variableDeclaration(NodeVector& elts, VarDeclKind kind, + TokenPos* pos, MutableHandleValue dst); + + /* + * patterns + */ + + [[nodiscard]] bool arrayPattern(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool objectPattern(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst); + + [[nodiscard]] bool propertyPattern(HandleValue key, HandleValue patt, + bool isShorthand, TokenPos* pos, + MutableHandleValue dst); +}; + +} /* anonymous namespace */ + +bool NodeBuilder::createNode(ASTType type, TokenPos* pos, + MutableHandleObject dst) { + MOZ_ASSERT(type > AST_ERROR && type < AST_LIMIT); + + RootedValue tv(cx); + Rooted<PlainObject*> node(cx, NewPlainObject(cx)); + if (!node || !setNodeLoc(node, pos) || !atomValue(nodeTypeNames[type], &tv) || + !defineProperty(node, "type", tv)) { + return false; + } + + dst.set(node); + return true; +} + +bool NodeBuilder::newArray(NodeVector& elts, MutableHandleValue dst) { + const size_t len = elts.length(); + if (len > UINT32_MAX) { + ReportAllocationOverflow(fc); + return false; + } + RootedObject array(cx, NewDenseFullyAllocatedArray(cx, uint32_t(len))); + if (!array) { + return false; + } + + for (size_t i = 0; i < len; i++) { + RootedValue val(cx, elts[i]); + + MOZ_ASSERT_IF(val.isMagic(), val.whyMagic() == JS_SERIALIZE_NO_NODE); + + /* Represent "no node" as an array hole by not adding the value. */ + if (val.isMagic(JS_SERIALIZE_NO_NODE)) { + continue; + } + + if (!DefineDataElement(cx, array, i, val)) { + return false; + } + } + + dst.setObject(*array); + return true; +} + +bool NodeBuilder::newNodeLoc(TokenPos* pos, MutableHandleValue dst) { + if (!pos) { + dst.setNull(); + return true; + } + + RootedObject loc(cx); + RootedObject to(cx); + RootedValue val(cx); + + if (!newObject(&loc)) { + return false; + } + + dst.setObject(*loc); + + uint32_t startLineNum, startColumnIndex; + uint32_t endLineNum, endColumnIndex; + parser->tokenStream.computeLineAndColumn(pos->begin, &startLineNum, + &startColumnIndex); + parser->tokenStream.computeLineAndColumn(pos->end, &endLineNum, + &endColumnIndex); + + if (!newObject(&to)) { + return false; + } + val.setObject(*to); + if (!defineProperty(loc, "start", val)) { + return false; + } + val.setNumber(startLineNum); + if (!defineProperty(to, "line", val)) { + return false; + } + val.setNumber(startColumnIndex); + if (!defineProperty(to, "column", val)) { + return false; + } + + if (!newObject(&to)) { + return false; + } + val.setObject(*to); + if (!defineProperty(loc, "end", val)) { + return false; + } + val.setNumber(endLineNum); + if (!defineProperty(to, "line", val)) { + return false; + } + val.setNumber(endColumnIndex); + if (!defineProperty(to, "column", val)) { + return false; + } + + if (!defineProperty(loc, "source", srcval)) { + return false; + } + + return true; +} + +bool NodeBuilder::setNodeLoc(HandleObject node, TokenPos* pos) { + if (!saveLoc) { + return true; + } + + RootedValue loc(cx); + return newNodeLoc(pos, &loc) && defineProperty(node, "loc", loc); +} + +bool NodeBuilder::program(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst) { + return listNode(AST_PROGRAM, "body", elts, pos, dst); +} + +bool NodeBuilder::blockStatement(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst) { + return listNode(AST_BLOCK_STMT, "body", elts, pos, dst); +} + +bool NodeBuilder::expressionStatement(HandleValue expr, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_EXPR_STMT, pos, "expression", expr, dst); +} + +bool NodeBuilder::emptyStatement(TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_EMPTY_STMT, pos, dst); +} + +bool NodeBuilder::ifStatement(HandleValue test, HandleValue cons, + HandleValue alt, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_IF_STMT, pos, "test", test, "consequent", cons, + "alternate", alt, dst); +} + +bool NodeBuilder::breakStatement(HandleValue label, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_BREAK_STMT, pos, "label", label, dst); +} + +bool NodeBuilder::continueStatement(HandleValue label, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_CONTINUE_STMT, pos, "label", label, dst); +} + +bool NodeBuilder::labeledStatement(HandleValue label, HandleValue stmt, + TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_LAB_STMT, pos, "label", label, "body", stmt, dst); +} + +bool NodeBuilder::throwStatement(HandleValue arg, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_THROW_STMT, pos, "argument", arg, dst); +} + +bool NodeBuilder::returnStatement(HandleValue arg, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_RETURN_STMT, pos, "argument", arg, dst); +} + +bool NodeBuilder::forStatement(HandleValue init, HandleValue test, + HandleValue update, HandleValue stmt, + TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_FOR_STMT, pos, "init", init, "test", test, "update", + update, "body", stmt, dst); +} + +bool NodeBuilder::forInStatement(HandleValue var, HandleValue expr, + HandleValue stmt, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_FOR_IN_STMT, pos, "left", var, "right", expr, "body", stmt, + dst); +} + +bool NodeBuilder::forOfStatement(HandleValue var, HandleValue expr, + HandleValue stmt, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_FOR_OF_STMT, pos, "left", var, "right", expr, "body", stmt, + dst); +} + +bool NodeBuilder::withStatement(HandleValue expr, HandleValue stmt, + TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_WITH_STMT, pos, "object", expr, "body", stmt, dst); +} + +bool NodeBuilder::whileStatement(HandleValue test, HandleValue stmt, + TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_WHILE_STMT, pos, "test", test, "body", stmt, dst); +} + +bool NodeBuilder::doWhileStatement(HandleValue stmt, HandleValue test, + TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_DO_STMT, pos, "body", stmt, "test", test, dst); +} + +bool NodeBuilder::switchStatement(HandleValue disc, NodeVector& elts, + bool lexical, TokenPos* pos, + MutableHandleValue dst) { + RootedValue array(cx); + if (!newArray(elts, &array)) { + return false; + } + + RootedValue lexicalVal(cx, BooleanValue(lexical)); + return newNode(AST_SWITCH_STMT, pos, "discriminant", disc, "cases", array, + "lexical", lexicalVal, dst); +} + +bool NodeBuilder::tryStatement(HandleValue body, HandleValue handler, + HandleValue finally, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_TRY_STMT, pos, "block", body, "handler", handler, + "finalizer", finally, dst); +} + +bool NodeBuilder::debuggerStatement(TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_DEBUGGER_STMT, pos, dst); +} + +bool NodeBuilder::binaryExpression(BinaryOperator op, HandleValue left, + HandleValue right, TokenPos* pos, + MutableHandleValue dst) { + MOZ_ASSERT(op > BINOP_ERR && op < BINOP_LIMIT); + + RootedValue opName(cx); + if (!atomValue(binopNames[op], &opName)) { + return false; + } + + return newNode(AST_BINARY_EXPR, pos, "operator", opName, "left", left, + "right", right, dst); +} + +bool NodeBuilder::unaryExpression(UnaryOperator unop, HandleValue expr, + TokenPos* pos, MutableHandleValue dst) { + MOZ_ASSERT(unop > UNOP_ERR && unop < UNOP_LIMIT); + + RootedValue opName(cx); + if (!atomValue(unopNames[unop], &opName)) { + return false; + } + + RootedValue trueVal(cx, BooleanValue(true)); + return newNode(AST_UNARY_EXPR, pos, "operator", opName, "argument", expr, + "prefix", trueVal, dst); +} + +bool NodeBuilder::assignmentExpression(AssignmentOperator aop, HandleValue lhs, + HandleValue rhs, TokenPos* pos, + MutableHandleValue dst) { + MOZ_ASSERT(aop > AOP_ERR && aop < AOP_LIMIT); + + RootedValue opName(cx); + if (!atomValue(aopNames[aop], &opName)) { + return false; + } + + return newNode(AST_ASSIGN_EXPR, pos, "operator", opName, "left", lhs, "right", + rhs, dst); +} + +bool NodeBuilder::updateExpression(HandleValue expr, bool incr, bool prefix, + TokenPos* pos, MutableHandleValue dst) { + RootedValue opName(cx); + if (!atomValue(incr ? "++" : "--", &opName)) { + return false; + } + + RootedValue prefixVal(cx, BooleanValue(prefix)); + + return newNode(AST_UPDATE_EXPR, pos, "operator", opName, "argument", expr, + "prefix", prefixVal, dst); +} + +bool NodeBuilder::logicalExpression(ParseNodeKind pnk, HandleValue left, + HandleValue right, TokenPos* pos, + MutableHandleValue dst) { + RootedValue opName(cx); + switch (pnk) { + case ParseNodeKind::OrExpr: + if (!atomValue("||", &opName)) { + return false; + } + break; + case ParseNodeKind::CoalesceExpr: + if (!atomValue("??", &opName)) { + return false; + } + break; + case ParseNodeKind::AndExpr: + if (!atomValue("&&", &opName)) { + return false; + } + break; + default: + MOZ_CRASH("Unexpected ParseNodeKind: Must be `Or`, `And`, or `Coalesce`"); + } + + return newNode(AST_LOGICAL_EXPR, pos, "operator", opName, "left", left, + "right", right, dst); +} + +bool NodeBuilder::conditionalExpression(HandleValue test, HandleValue cons, + HandleValue alt, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_COND_EXPR, pos, "test", test, "consequent", cons, + "alternate", alt, dst); +} + +bool NodeBuilder::sequenceExpression(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst) { + return listNode(AST_LIST_EXPR, "expressions", elts, pos, dst); +} + +bool NodeBuilder::callExpression(HandleValue callee, NodeVector& args, + TokenPos* pos, MutableHandleValue dst, + bool isOptional) { + RootedValue array(cx); + if (!newArray(args, &array)) { + return false; + } + + return newNode(isOptional ? AST_OPT_CALL_EXPR : AST_CALL_EXPR, pos, "callee", + callee, "arguments", array, dst); +} + +bool NodeBuilder::newExpression(HandleValue callee, NodeVector& args, + TokenPos* pos, MutableHandleValue dst) { + RootedValue array(cx); + if (!newArray(args, &array)) { + return false; + } + + return newNode(AST_NEW_EXPR, pos, "callee", callee, "arguments", array, dst); +} + +bool NodeBuilder::memberExpression(bool computed, HandleValue expr, + HandleValue member, TokenPos* pos, + MutableHandleValue dst, + bool isOptional /* = false */) { + RootedValue computedVal(cx, BooleanValue(computed)); + + return newNode(isOptional ? AST_OPT_MEMBER_EXPR : AST_MEMBER_EXPR, pos, + "object", expr, "property", member, "computed", computedVal, + dst); +} + +bool NodeBuilder::arrayExpression(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst) { + return listNode(AST_ARRAY_EXPR, "elements", elts, pos, dst); +} + +bool NodeBuilder::callSiteObj(NodeVector& raw, NodeVector& cooked, + TokenPos* pos, MutableHandleValue dst) { + RootedValue rawVal(cx); + if (!newArray(raw, &rawVal)) { + return false; + } + + RootedValue cookedVal(cx); + if (!newArray(cooked, &cookedVal)) { + return false; + } + + return newNode(AST_CALL_SITE_OBJ, pos, "raw", rawVal, "cooked", cookedVal, + dst); +} + +bool NodeBuilder::taggedTemplate(HandleValue callee, NodeVector& args, + TokenPos* pos, MutableHandleValue dst) { + RootedValue array(cx); + if (!newArray(args, &array)) { + return false; + } + + return newNode(AST_TAGGED_TEMPLATE, pos, "callee", callee, "arguments", array, + dst); +} + +bool NodeBuilder::templateLiteral(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst) { + return listNode(AST_TEMPLATE_LITERAL, "elements", elts, pos, dst); +} + +bool NodeBuilder::computedName(HandleValue name, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_COMPUTED_NAME, pos, "name", name, dst); +} + +bool NodeBuilder::spreadExpression(HandleValue expr, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_SPREAD_EXPR, pos, "expression", expr, dst); +} + +bool NodeBuilder::optionalExpression(HandleValue expr, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_OPTIONAL_EXPR, pos, "expression", expr, dst); +} + +bool NodeBuilder::deleteOptionalExpression(HandleValue expr, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_DELETE_OPTIONAL_EXPR, pos, "expression", expr, dst); +} + +bool NodeBuilder::propertyPattern(HandleValue key, HandleValue patt, + bool isShorthand, TokenPos* pos, + MutableHandleValue dst) { + RootedValue kindName(cx); + if (!atomValue("init", &kindName)) { + return false; + } + + RootedValue isShorthandVal(cx, BooleanValue(isShorthand)); + + return newNode(AST_PROP_PATT, pos, "key", key, "value", patt, "kind", + kindName, "shorthand", isShorthandVal, dst); +} + +bool NodeBuilder::prototypeMutation(HandleValue val, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_PROTOTYPEMUTATION, pos, "value", val, dst); +} + +bool NodeBuilder::propertyInitializer(HandleValue key, HandleValue val, + PropKind kind, bool isShorthand, + bool isMethod, TokenPos* pos, + MutableHandleValue dst) { + RootedValue kindName(cx); + if (!atomValue(kind == PROP_INIT ? "init" + : kind == PROP_GETTER ? "get" + : "set", + &kindName)) { + return false; + } + + RootedValue isShorthandVal(cx, BooleanValue(isShorthand)); + RootedValue isMethodVal(cx, BooleanValue(isMethod)); + + return newNode(AST_PROPERTY, pos, "key", key, "value", val, "kind", kindName, + "method", isMethodVal, "shorthand", isShorthandVal, dst); +} + +bool NodeBuilder::objectExpression(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst) { + return listNode(AST_OBJECT_EXPR, "properties", elts, pos, dst); +} + +#ifdef ENABLE_RECORD_TUPLE +bool NodeBuilder::recordExpression(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst) { + return listNode(AST_RECORD_EXPR, "properties", elts, pos, dst); +} + +bool NodeBuilder::tupleExpression(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst) { + return listNode(AST_TUPLE_EXPR, "elements", elts, pos, dst); +} +#endif + +bool NodeBuilder::thisExpression(TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_THIS_EXPR, pos, dst); +} + +bool NodeBuilder::yieldExpression(HandleValue arg, YieldKind kind, + TokenPos* pos, MutableHandleValue dst) { + RootedValue delegateVal(cx); + switch (kind) { + case Delegating: + delegateVal = BooleanValue(true); + break; + case NotDelegating: + delegateVal = BooleanValue(false); + break; + } + return newNode(AST_YIELD_EXPR, pos, "argument", arg, "delegate", delegateVal, + dst); +} + +bool NodeBuilder::moduleRequest(HandleValue moduleSpec, NodeVector& assertions, + TokenPos* pos, MutableHandleValue dst) { + RootedValue array(cx); + if (!newArray(assertions, &array)) { + return false; + } + + return newNode(AST_MODULE_REQUEST, pos, "source", moduleSpec, "assertions", + array, dst); +} + +bool NodeBuilder::importAssertion(HandleValue key, HandleValue value, + TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_IMPORT_ASSERTION, pos, "key", key, "value", value, dst); +} + +bool NodeBuilder::importDeclaration(NodeVector& elts, HandleValue moduleRequest, + TokenPos* pos, MutableHandleValue dst) { + RootedValue array(cx); + if (!newArray(elts, &array)) { + return false; + } + + return newNode(AST_IMPORT_DECL, pos, "specifiers", array, "moduleRequest", + moduleRequest, dst); +} + +bool NodeBuilder::importSpecifier(HandleValue importName, + HandleValue bindingName, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_IMPORT_SPEC, pos, "id", importName, "name", bindingName, + dst); +} + +bool NodeBuilder::importNamespaceSpecifier(HandleValue bindingName, + TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_IMPORT_NAMESPACE_SPEC, pos, "name", bindingName, dst); +} + +bool NodeBuilder::exportDeclaration(HandleValue decl, NodeVector& elts, + HandleValue moduleRequest, + HandleValue isDefault, TokenPos* pos, + MutableHandleValue dst) { + RootedValue array(cx, NullValue()); + if (decl.isNull() && !newArray(elts, &array)) { + return false; + } + + return newNode(AST_EXPORT_DECL, pos, "declaration", decl, "specifiers", array, + "moduleRequest", moduleRequest, "isDefault", isDefault, dst); +} + +bool NodeBuilder::exportSpecifier(HandleValue bindingName, + HandleValue exportName, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_EXPORT_SPEC, pos, "id", bindingName, "name", exportName, + dst); +} + +bool NodeBuilder::exportNamespaceSpecifier(HandleValue exportName, + TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_EXPORT_NAMESPACE_SPEC, pos, "name", exportName, dst); +} + +bool NodeBuilder::exportBatchSpecifier(TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_EXPORT_BATCH_SPEC, pos, dst); +} + +bool NodeBuilder::variableDeclaration(NodeVector& elts, VarDeclKind kind, + TokenPos* pos, MutableHandleValue dst) { + MOZ_ASSERT(kind > VARDECL_ERR && kind < VARDECL_LIMIT); + + RootedValue array(cx), kindName(cx); + if (!newArray(elts, &array) || !atomValue(kind == VARDECL_CONST ? "const" + : kind == VARDECL_LET ? "let" + : "var", + &kindName)) { + return false; + } + + return newNode(AST_VAR_DECL, pos, "kind", kindName, "declarations", array, + dst); +} + +bool NodeBuilder::variableDeclarator(HandleValue id, HandleValue init, + TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_VAR_DTOR, pos, "id", id, "init", init, dst); +} + +bool NodeBuilder::switchCase(HandleValue expr, NodeVector& elts, TokenPos* pos, + MutableHandleValue dst) { + RootedValue array(cx); + if (!newArray(elts, &array)) { + return false; + } + + return newNode(AST_CASE, pos, "test", expr, "consequent", array, dst); +} + +bool NodeBuilder::catchClause(HandleValue var, HandleValue body, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_CATCH, pos, "param", var, "body", body, dst); +} + +bool NodeBuilder::literal(HandleValue val, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_LITERAL, pos, "value", val, dst); +} + +bool NodeBuilder::identifier(HandleValue name, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_IDENTIFIER, pos, "name", name, dst); +} + +bool NodeBuilder::objectPattern(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst) { + return listNode(AST_OBJECT_PATT, "properties", elts, pos, dst); +} + +bool NodeBuilder::arrayPattern(NodeVector& elts, TokenPos* pos, + MutableHandleValue dst) { + return listNode(AST_ARRAY_PATT, "elements", elts, pos, dst); +} + +bool NodeBuilder::function(ASTType type, TokenPos* pos, HandleValue id, + NodeVector& args, NodeVector& defaults, + HandleValue body, HandleValue rest, + GeneratorStyle generatorStyle, bool isAsync, + bool isExpression, MutableHandleValue dst) { + RootedValue array(cx), defarray(cx); + if (!newArray(args, &array)) { + return false; + } + if (!newArray(defaults, &defarray)) { + return false; + } + + bool isGenerator = generatorStyle != GeneratorStyle::None; + RootedValue isGeneratorVal(cx, BooleanValue(isGenerator)); + RootedValue isAsyncVal(cx, BooleanValue(isAsync)); + RootedValue isExpressionVal(cx, BooleanValue(isExpression)); + + if (isGenerator) { + MOZ_ASSERT(generatorStyle == GeneratorStyle::ES6); + JSAtom* styleStr = Atomize(cx, "es6", 3); + if (!styleStr) { + return false; + } + RootedValue styleVal(cx, StringValue(styleStr)); + return newNode(type, pos, "id", id, "params", array, "defaults", defarray, + "body", body, "rest", rest, "generator", isGeneratorVal, + "async", isAsyncVal, "style", styleVal, "expression", + isExpressionVal, dst); + } + + return newNode(type, pos, "id", id, "params", array, "defaults", defarray, + "body", body, "rest", rest, "generator", isGeneratorVal, + "async", isAsyncVal, "expression", isExpressionVal, dst); +} + +bool NodeBuilder::classMethod(HandleValue name, HandleValue body, PropKind kind, + bool isStatic, TokenPos* pos, + MutableHandleValue dst) { + RootedValue kindName(cx); + if (!atomValue(kind == PROP_INIT ? "method" + : kind == PROP_GETTER ? "get" + : "set", + &kindName)) { + return false; + } + + RootedValue isStaticVal(cx, BooleanValue(isStatic)); + return newNode(AST_CLASS_METHOD, pos, "name", name, "body", body, "kind", + kindName, "static", isStaticVal, dst); +} + +bool NodeBuilder::classField(HandleValue name, HandleValue initializer, + TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_CLASS_FIELD, pos, "name", name, "init", initializer, dst); +} + +bool NodeBuilder::staticClassBlock(HandleValue body, TokenPos* pos, + MutableHandleValue dst) { + return newNode(AST_STATIC_CLASS_BLOCK, pos, "body", body, dst); +} + +bool NodeBuilder::classMembers(NodeVector& members, MutableHandleValue dst) { + return newArray(members, dst); +} + +bool NodeBuilder::classDefinition(bool expr, HandleValue name, + HandleValue heritage, HandleValue block, + TokenPos* pos, MutableHandleValue dst) { + ASTType type = expr ? AST_CLASS_EXPR : AST_CLASS_STMT; + return newNode(type, pos, "id", name, "superClass", heritage, "body", block, + dst); +} + +bool NodeBuilder::metaProperty(HandleValue meta, HandleValue property, + TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_METAPROPERTY, pos, "meta", meta, "property", property, + dst); +} + +bool NodeBuilder::callImportExpression(HandleValue ident, NodeVector& args, + TokenPos* pos, MutableHandleValue dst) { + RootedValue array(cx); + if (!newArray(args, &array)) { + return false; + } + + return newNode(AST_CALL_IMPORT, pos, "ident", ident, "arguments", array, dst); +} + +bool NodeBuilder::super(TokenPos* pos, MutableHandleValue dst) { + return newNode(AST_SUPER, pos, dst); +} + +namespace { + +/* + * Serialization of parse nodes to JavaScript objects. + * + * All serialization methods take a non-nullable ParseNode pointer. + */ +class ASTSerializer { + JSContext* cx; + FrontendContext* fc; + Parser<FullParseHandler, char16_t>* parser; + NodeBuilder builder; + DebugOnly<uint32_t> lineno; + + Value unrootedAtomContents(JSAtom* atom) { + return StringValue(atom ? atom : cx->names().empty); + } + + BinaryOperator binop(ParseNodeKind kind); + UnaryOperator unop(ParseNodeKind kind); + AssignmentOperator aop(ParseNodeKind kind); + + bool statements(ListNode* stmtList, NodeVector& elts); + bool expressions(ListNode* exprList, NodeVector& elts); + bool leftAssociate(ListNode* node, MutableHandleValue dst); + bool rightAssociate(ListNode* node, MutableHandleValue dst); + bool functionArgs(ParamsBodyNode* pn, NodeVector& args, NodeVector& defaults, + MutableHandleValue rest); + + bool sourceElement(ParseNode* pn, MutableHandleValue dst); + + bool declaration(ParseNode* pn, MutableHandleValue dst); + bool variableDeclaration(ListNode* declList, bool lexical, + MutableHandleValue dst); + bool variableDeclarator(ParseNode* pn, MutableHandleValue dst); + bool importDeclaration(BinaryNode* importNode, MutableHandleValue dst); + bool importSpecifier(BinaryNode* importSpec, MutableHandleValue dst); + bool importNamespaceSpecifier(UnaryNode* importSpec, MutableHandleValue dst); + bool exportDeclaration(ParseNode* exportNode, MutableHandleValue dst); + bool exportSpecifier(BinaryNode* exportSpec, MutableHandleValue dst); + bool exportNamespaceSpecifier(UnaryNode* exportSpec, MutableHandleValue dst); + bool classDefinition(ClassNode* pn, bool expr, MutableHandleValue dst); + bool importAssertions(ListNode* assertionList, NodeVector& assertions); + + bool optStatement(ParseNode* pn, MutableHandleValue dst) { + if (!pn) { + dst.setMagic(JS_SERIALIZE_NO_NODE); + return true; + } + return statement(pn, dst); + } + + bool forInit(ParseNode* pn, MutableHandleValue dst); + bool forIn(ForNode* loop, ParseNode* iterExpr, HandleValue var, + HandleValue stmt, MutableHandleValue dst); + bool forOf(ForNode* loop, ParseNode* iterExpr, HandleValue var, + HandleValue stmt, MutableHandleValue dst); + bool statement(ParseNode* pn, MutableHandleValue dst); + bool blockStatement(ListNode* node, MutableHandleValue dst); + bool switchStatement(SwitchStatement* switchStmt, MutableHandleValue dst); + bool switchCase(CaseClause* caseClause, MutableHandleValue dst); + bool tryStatement(TryNode* tryNode, MutableHandleValue dst); + bool catchClause(BinaryNode* catchClause, MutableHandleValue dst); + + bool optExpression(ParseNode* pn, MutableHandleValue dst) { + if (!pn) { + dst.setMagic(JS_SERIALIZE_NO_NODE); + return true; + } + return expression(pn, dst); + } + + bool expression(ParseNode* pn, MutableHandleValue dst); + + bool propertyName(ParseNode* key, MutableHandleValue dst); + bool property(ParseNode* pn, MutableHandleValue dst); + + bool classMethod(ClassMethod* classMethod, MutableHandleValue dst); + bool classField(ClassField* classField, MutableHandleValue dst); + bool staticClassBlock(StaticClassBlock* staticClassBlock, + MutableHandleValue dst); + + bool optIdentifier(Handle<JSAtom*> atom, TokenPos* pos, + MutableHandleValue dst) { + if (!atom) { + dst.setMagic(JS_SERIALIZE_NO_NODE); + return true; + } + return identifier(atom, pos, dst); + } + + bool identifier(Handle<JSAtom*> atom, TokenPos* pos, MutableHandleValue dst); + bool identifier(NameNode* id, MutableHandleValue dst); + bool identifierOrLiteral(ParseNode* id, MutableHandleValue dst); + bool literal(ParseNode* pn, MutableHandleValue dst); + + bool optPattern(ParseNode* pn, MutableHandleValue dst) { + if (!pn) { + dst.setMagic(JS_SERIALIZE_NO_NODE); + return true; + } + return pattern(pn, dst); + } + + bool pattern(ParseNode* pn, MutableHandleValue dst); + bool arrayPattern(ListNode* array, MutableHandleValue dst); + bool objectPattern(ListNode* obj, MutableHandleValue dst); + + bool function(FunctionNode* funNode, ASTType type, MutableHandleValue dst); + bool functionArgsAndBody(ParamsBodyNode* pn, NodeVector& args, + NodeVector& defaults, bool isAsync, + bool isExpression, MutableHandleValue body, + MutableHandleValue rest); + bool functionBody(ParseNode* pn, TokenPos* pos, MutableHandleValue dst); + + public: + ASTSerializer(JSContext* c, FrontendContext* f, bool l, char const* src, + uint32_t ln) + : cx(c), + fc(f), + parser(nullptr), + builder(c, f, l, src) +#ifdef DEBUG + , + lineno(ln) +#endif + { + } + + bool init() { return builder.init(); } + + void setParser(frontend::Parser<frontend::FullParseHandler, char16_t>* p) { + parser = p; + builder.setParser(p); + } + + bool program(ListNode* node, MutableHandleValue dst); +}; + +} /* anonymous namespace */ + +AssignmentOperator ASTSerializer::aop(ParseNodeKind kind) { + switch (kind) { + case ParseNodeKind::AssignExpr: + return AOP_ASSIGN; + case ParseNodeKind::AddAssignExpr: + return AOP_PLUS; + case ParseNodeKind::SubAssignExpr: + return AOP_MINUS; + case ParseNodeKind::MulAssignExpr: + return AOP_STAR; + case ParseNodeKind::DivAssignExpr: + return AOP_DIV; + case ParseNodeKind::ModAssignExpr: + return AOP_MOD; + case ParseNodeKind::PowAssignExpr: + return AOP_POW; + case ParseNodeKind::LshAssignExpr: + return AOP_LSH; + case ParseNodeKind::RshAssignExpr: + return AOP_RSH; + case ParseNodeKind::UrshAssignExpr: + return AOP_URSH; + case ParseNodeKind::BitOrAssignExpr: + return AOP_BITOR; + case ParseNodeKind::BitXorAssignExpr: + return AOP_BITXOR; + case ParseNodeKind::BitAndAssignExpr: + return AOP_BITAND; + case ParseNodeKind::CoalesceAssignExpr: + return AOP_COALESCE; + case ParseNodeKind::OrAssignExpr: + return AOP_OR; + case ParseNodeKind::AndAssignExpr: + return AOP_AND; + default: + return AOP_ERR; + } +} + +UnaryOperator ASTSerializer::unop(ParseNodeKind kind) { + if (IsDeleteKind(kind)) { + return UNOP_DELETE; + } + + if (IsTypeofKind(kind)) { + return UNOP_TYPEOF; + } + + switch (kind) { + case ParseNodeKind::AwaitExpr: + return UNOP_AWAIT; + case ParseNodeKind::NegExpr: + return UNOP_NEG; + case ParseNodeKind::PosExpr: + return UNOP_POS; + case ParseNodeKind::NotExpr: + return UNOP_NOT; + case ParseNodeKind::BitNotExpr: + return UNOP_BITNOT; + case ParseNodeKind::VoidExpr: + return UNOP_VOID; + default: + return UNOP_ERR; + } +} + +BinaryOperator ASTSerializer::binop(ParseNodeKind kind) { + switch (kind) { + case ParseNodeKind::LshExpr: + return BINOP_LSH; + case ParseNodeKind::RshExpr: + return BINOP_RSH; + case ParseNodeKind::UrshExpr: + return BINOP_URSH; + case ParseNodeKind::LtExpr: + return BINOP_LT; + case ParseNodeKind::LeExpr: + return BINOP_LE; + case ParseNodeKind::GtExpr: + return BINOP_GT; + case ParseNodeKind::GeExpr: + return BINOP_GE; + case ParseNodeKind::EqExpr: + return BINOP_EQ; + case ParseNodeKind::NeExpr: + return BINOP_NE; + case ParseNodeKind::StrictEqExpr: + return BINOP_STRICTEQ; + case ParseNodeKind::StrictNeExpr: + return BINOP_STRICTNE; + case ParseNodeKind::AddExpr: + return BINOP_ADD; + case ParseNodeKind::SubExpr: + return BINOP_SUB; + case ParseNodeKind::MulExpr: + return BINOP_STAR; + case ParseNodeKind::DivExpr: + return BINOP_DIV; + case ParseNodeKind::ModExpr: + return BINOP_MOD; + case ParseNodeKind::PowExpr: + return BINOP_POW; + case ParseNodeKind::BitOrExpr: + return BINOP_BITOR; + case ParseNodeKind::BitXorExpr: + return BINOP_BITXOR; + case ParseNodeKind::BitAndExpr: + return BINOP_BITAND; + case ParseNodeKind::InExpr: + case ParseNodeKind::PrivateInExpr: + return BINOP_IN; + case ParseNodeKind::InstanceOfExpr: + return BINOP_INSTANCEOF; + case ParseNodeKind::CoalesceExpr: + return BINOP_COALESCE; + default: + return BINOP_ERR; + } +} + +bool ASTSerializer::statements(ListNode* stmtList, NodeVector& elts) { + MOZ_ASSERT(stmtList->isKind(ParseNodeKind::StatementList)); + + if (!elts.reserve(stmtList->count())) { + return false; + } + + for (ParseNode* stmt : stmtList->contents()) { + MOZ_ASSERT(stmtList->pn_pos.encloses(stmt->pn_pos)); + + RootedValue elt(cx); + if (!sourceElement(stmt, &elt)) { + return false; + } + elts.infallibleAppend(elt); + } + + return true; +} + +bool ASTSerializer::expressions(ListNode* exprList, NodeVector& elts) { + if (!elts.reserve(exprList->count())) { + return false; + } + + for (ParseNode* expr : exprList->contents()) { + MOZ_ASSERT(exprList->pn_pos.encloses(expr->pn_pos)); + + RootedValue elt(cx); + if (!expression(expr, &elt)) { + return false; + } + elts.infallibleAppend(elt); + } + + return true; +} + +bool ASTSerializer::blockStatement(ListNode* node, MutableHandleValue dst) { + MOZ_ASSERT(node->isKind(ParseNodeKind::StatementList)); + + NodeVector stmts(cx); + return statements(node, stmts) && + builder.blockStatement(stmts, &node->pn_pos, dst); +} + +bool ASTSerializer::program(ListNode* node, MutableHandleValue dst) { +#ifdef DEBUG + { + const TokenStreamAnyChars& anyChars = parser->anyChars; + auto lineToken = anyChars.lineToken(node->pn_pos.begin); + MOZ_ASSERT(anyChars.lineNumber(lineToken) == lineno); + } +#endif + + NodeVector stmts(cx); + return statements(node, stmts) && builder.program(stmts, &node->pn_pos, dst); +} + +bool ASTSerializer::sourceElement(ParseNode* pn, MutableHandleValue dst) { + /* SpiderMonkey allows declarations even in pure statement contexts. */ + return statement(pn, dst); +} + +bool ASTSerializer::declaration(ParseNode* pn, MutableHandleValue dst) { + MOZ_ASSERT(pn->isKind(ParseNodeKind::Function) || + pn->isKind(ParseNodeKind::VarStmt) || + pn->isKind(ParseNodeKind::LetDecl) || + pn->isKind(ParseNodeKind::ConstDecl)); + + switch (pn->getKind()) { + case ParseNodeKind::Function: + return function(&pn->as<FunctionNode>(), AST_FUNC_DECL, dst); + + case ParseNodeKind::VarStmt: + return variableDeclaration(&pn->as<ListNode>(), false, dst); + + default: + MOZ_ASSERT(pn->isKind(ParseNodeKind::LetDecl) || + pn->isKind(ParseNodeKind::ConstDecl)); + return variableDeclaration(&pn->as<ListNode>(), true, dst); + } +} + +bool ASTSerializer::variableDeclaration(ListNode* declList, bool lexical, + MutableHandleValue dst) { + MOZ_ASSERT_IF(lexical, declList->isKind(ParseNodeKind::LetDecl) || + declList->isKind(ParseNodeKind::ConstDecl)); + MOZ_ASSERT_IF(!lexical, declList->isKind(ParseNodeKind::VarStmt)); + + VarDeclKind kind = VARDECL_ERR; + // Treat both the toplevel const binding (secretly var-like) and the lexical + // const the same way + if (lexical) { + kind = + declList->isKind(ParseNodeKind::LetDecl) ? VARDECL_LET : VARDECL_CONST; + } else { + kind = + declList->isKind(ParseNodeKind::VarStmt) ? VARDECL_VAR : VARDECL_CONST; + } + + NodeVector dtors(cx); + if (!dtors.reserve(declList->count())) { + return false; + } + for (ParseNode* decl : declList->contents()) { + RootedValue child(cx); + if (!variableDeclarator(decl, &child)) { + return false; + } + dtors.infallibleAppend(child); + } + return builder.variableDeclaration(dtors, kind, &declList->pn_pos, dst); +} + +bool ASTSerializer::variableDeclarator(ParseNode* pn, MutableHandleValue dst) { + ParseNode* patternNode; + ParseNode* initNode; + + if (pn->isKind(ParseNodeKind::Name)) { + patternNode = pn; + initNode = nullptr; + } else if (pn->isKind(ParseNodeKind::AssignExpr)) { + AssignmentNode* assignNode = &pn->as<AssignmentNode>(); + patternNode = assignNode->left(); + initNode = assignNode->right(); + MOZ_ASSERT(pn->pn_pos.encloses(patternNode->pn_pos)); + MOZ_ASSERT(pn->pn_pos.encloses(initNode->pn_pos)); + } else { + /* This happens for a destructuring declarator in a for-in/of loop. */ + patternNode = pn; + initNode = nullptr; + } + + RootedValue patternVal(cx), init(cx); + return pattern(patternNode, &patternVal) && optExpression(initNode, &init) && + builder.variableDeclarator(patternVal, init, &pn->pn_pos, dst); +} + +bool ASTSerializer::importDeclaration(BinaryNode* importNode, + MutableHandleValue dst) { + MOZ_ASSERT(importNode->isKind(ParseNodeKind::ImportDecl)); + + ListNode* specList = &importNode->left()->as<ListNode>(); + MOZ_ASSERT(specList->isKind(ParseNodeKind::ImportSpecList)); + + auto* moduleRequest = &importNode->right()->as<BinaryNode>(); + MOZ_ASSERT(moduleRequest->isKind(ParseNodeKind::ImportModuleRequest)); + + ParseNode* moduleSpecNode = moduleRequest->left(); + MOZ_ASSERT(moduleSpecNode->isKind(ParseNodeKind::StringExpr)); + + auto* assertionList = &moduleRequest->right()->as<ListNode>(); + MOZ_ASSERT(assertionList->isKind(ParseNodeKind::ImportAssertionList)); + + NodeVector elts(cx); + if (!elts.reserve(specList->count())) { + return false; + } + + for (ParseNode* item : specList->contents()) { + RootedValue elt(cx); + if (item->is<UnaryNode>()) { + auto* spec = &item->as<UnaryNode>(); + if (!importNamespaceSpecifier(spec, &elt)) { + return false; + } + } else { + auto* spec = &item->as<BinaryNode>(); + if (!importSpecifier(spec, &elt)) { + return false; + } + } + elts.infallibleAppend(elt); + } + + RootedValue moduleSpec(cx); + if (!literal(moduleSpecNode, &moduleSpec)) { + return false; + } + + NodeVector assertions(cx); + if (!importAssertions(assertionList, assertions)) { + return false; + } + + RootedValue moduleRequestValue(cx); + if (!builder.moduleRequest(moduleSpec, assertions, &importNode->pn_pos, + &moduleRequestValue)) { + return false; + } + + return builder.importDeclaration(elts, moduleRequestValue, + &importNode->pn_pos, dst); +} + +bool ASTSerializer::importSpecifier(BinaryNode* importSpec, + MutableHandleValue dst) { + MOZ_ASSERT(importSpec->isKind(ParseNodeKind::ImportSpec)); + NameNode* importNameNode = &importSpec->left()->as<NameNode>(); + NameNode* bindingNameNode = &importSpec->right()->as<NameNode>(); + + RootedValue importName(cx); + RootedValue bindingName(cx); + return identifierOrLiteral(importNameNode, &importName) && + identifier(bindingNameNode, &bindingName) && + builder.importSpecifier(importName, bindingName, &importSpec->pn_pos, + dst); +} + +bool ASTSerializer::importNamespaceSpecifier(UnaryNode* importSpec, + MutableHandleValue dst) { + MOZ_ASSERT(importSpec->isKind(ParseNodeKind::ImportNamespaceSpec)); + NameNode* bindingNameNode = &importSpec->kid()->as<NameNode>(); + + RootedValue bindingName(cx); + return identifier(bindingNameNode, &bindingName) && + builder.importNamespaceSpecifier(bindingName, &importSpec->pn_pos, + dst); +} + +bool ASTSerializer::exportDeclaration(ParseNode* exportNode, + MutableHandleValue dst) { + MOZ_ASSERT(exportNode->isKind(ParseNodeKind::ExportStmt) || + exportNode->isKind(ParseNodeKind::ExportFromStmt) || + exportNode->isKind(ParseNodeKind::ExportDefaultStmt)); + MOZ_ASSERT_IF(exportNode->isKind(ParseNodeKind::ExportStmt), + exportNode->is<UnaryNode>()); + MOZ_ASSERT_IF(exportNode->isKind(ParseNodeKind::ExportFromStmt), + exportNode->as<BinaryNode>().right()->isKind( + ParseNodeKind::ImportModuleRequest)); + + RootedValue decl(cx, NullValue()); + NodeVector elts(cx); + + ParseNode* kid = exportNode->isKind(ParseNodeKind::ExportStmt) + ? exportNode->as<UnaryNode>().kid() + : exportNode->as<BinaryNode>().left(); + switch (ParseNodeKind kind = kid->getKind()) { + case ParseNodeKind::ExportSpecList: { + ListNode* specList = &kid->as<ListNode>(); + if (!elts.reserve(specList->count())) { + return false; + } + + for (ParseNode* spec : specList->contents()) { + RootedValue elt(cx); + if (spec->isKind(ParseNodeKind::ExportSpec)) { + if (!exportSpecifier(&spec->as<BinaryNode>(), &elt)) { + return false; + } + } else if (spec->isKind(ParseNodeKind::ExportNamespaceSpec)) { + if (!exportNamespaceSpecifier(&spec->as<UnaryNode>(), &elt)) { + return false; + } + } else { + MOZ_ASSERT(spec->isKind(ParseNodeKind::ExportBatchSpecStmt)); + if (!builder.exportBatchSpecifier(&exportNode->pn_pos, &elt)) { + return false; + } + } + elts.infallibleAppend(elt); + } + break; + } + + case ParseNodeKind::Function: + if (!function(&kid->as<FunctionNode>(), AST_FUNC_DECL, &decl)) { + return false; + } + break; + + case ParseNodeKind::ClassDecl: + if (!classDefinition(&kid->as<ClassNode>(), false, &decl)) { + return false; + } + break; + + case ParseNodeKind::VarStmt: + case ParseNodeKind::ConstDecl: + case ParseNodeKind::LetDecl: + if (!variableDeclaration(&kid->as<ListNode>(), + kind != ParseNodeKind::VarStmt, &decl)) { + return false; + } + break; + + default: + if (!expression(kid, &decl)) { + return false; + } + break; + } + + RootedValue moduleSpec(cx, NullValue()); + RootedValue moduleRequestValue(cx, NullValue()); + if (exportNode->isKind(ParseNodeKind::ExportFromStmt)) { + ParseNode* moduleRequest = exportNode->as<BinaryNode>().right(); + if (!literal(moduleRequest->as<BinaryNode>().left(), &moduleSpec)) { + return false; + } + + auto* assertionList = + &moduleRequest->as<BinaryNode>().right()->as<ListNode>(); + MOZ_ASSERT(assertionList->isKind(ParseNodeKind::ImportAssertionList)); + + NodeVector assertions(cx); + if (!importAssertions(assertionList, assertions)) { + return false; + } + + if (!builder.moduleRequest(moduleSpec, assertions, &exportNode->pn_pos, + &moduleRequestValue)) { + return false; + } + } + + RootedValue isDefault(cx, BooleanValue(false)); + if (exportNode->isKind(ParseNodeKind::ExportDefaultStmt)) { + isDefault.setBoolean(true); + } + + return builder.exportDeclaration(decl, elts, moduleRequestValue, isDefault, + &exportNode->pn_pos, dst); +} + +bool ASTSerializer::exportSpecifier(BinaryNode* exportSpec, + MutableHandleValue dst) { + MOZ_ASSERT(exportSpec->isKind(ParseNodeKind::ExportSpec)); + NameNode* bindingNameNode = &exportSpec->left()->as<NameNode>(); + NameNode* exportNameNode = &exportSpec->right()->as<NameNode>(); + + RootedValue bindingName(cx); + RootedValue exportName(cx); + return identifierOrLiteral(bindingNameNode, &bindingName) && + identifierOrLiteral(exportNameNode, &exportName) && + builder.exportSpecifier(bindingName, exportName, &exportSpec->pn_pos, + dst); +} + +bool ASTSerializer::exportNamespaceSpecifier(UnaryNode* exportSpec, + MutableHandleValue dst) { + MOZ_ASSERT(exportSpec->isKind(ParseNodeKind::ExportNamespaceSpec)); + NameNode* exportNameNode = &exportSpec->kid()->as<NameNode>(); + + RootedValue exportName(cx); + return identifierOrLiteral(exportNameNode, &exportName) && + builder.exportNamespaceSpecifier(exportName, &exportSpec->pn_pos, dst); +} + +bool ASTSerializer::importAssertions(ListNode* assertionList, + NodeVector& assertions) { + for (ParseNode* assertionItem : assertionList->contents()) { + BinaryNode* assertionNode = &assertionItem->as<BinaryNode>(); + MOZ_ASSERT(assertionNode->isKind(ParseNodeKind::ImportAssertion)); + + NameNode* keyNameNode = &assertionNode->left()->as<NameNode>(); + NameNode* valueNameNode = &assertionNode->right()->as<NameNode>(); + + RootedValue key(cx); + if (!identifierOrLiteral(keyNameNode, &key)) { + return false; + } + + RootedValue value(cx); + if (!literal(valueNameNode, &value)) { + return false; + } + + RootedValue assertion(cx); + if (!builder.importAssertion(key, value, &assertionNode->pn_pos, + &assertion)) { + return false; + } + + if (!assertions.append(assertion)) { + return false; + } + } + + return true; +} + +bool ASTSerializer::switchCase(CaseClause* caseClause, MutableHandleValue dst) { + MOZ_ASSERT_IF( + caseClause->caseExpression(), + caseClause->pn_pos.encloses(caseClause->caseExpression()->pn_pos)); + MOZ_ASSERT(caseClause->pn_pos.encloses(caseClause->statementList()->pn_pos)); + + NodeVector stmts(cx); + RootedValue expr(cx); + return optExpression(caseClause->caseExpression(), &expr) && + statements(caseClause->statementList(), stmts) && + builder.switchCase(expr, stmts, &caseClause->pn_pos, dst); +} + +bool ASTSerializer::switchStatement(SwitchStatement* switchStmt, + MutableHandleValue dst) { + MOZ_ASSERT(switchStmt->pn_pos.encloses(switchStmt->discriminant().pn_pos)); + MOZ_ASSERT( + switchStmt->pn_pos.encloses(switchStmt->lexicalForCaseList().pn_pos)); + + RootedValue disc(cx); + if (!expression(&switchStmt->discriminant(), &disc)) { + return false; + } + + ListNode* caseList = + &switchStmt->lexicalForCaseList().scopeBody()->as<ListNode>(); + + NodeVector cases(cx); + if (!cases.reserve(caseList->count())) { + return false; + } + + for (ParseNode* item : caseList->contents()) { + CaseClause* caseClause = &item->as<CaseClause>(); + RootedValue child(cx); + if (!switchCase(caseClause, &child)) { + return false; + } + cases.infallibleAppend(child); + } + + // `lexical` field is always true. + return builder.switchStatement(disc, cases, true, &switchStmt->pn_pos, dst); +} + +bool ASTSerializer::catchClause(BinaryNode* catchClause, + MutableHandleValue dst) { + MOZ_ASSERT(catchClause->isKind(ParseNodeKind::Catch)); + + ParseNode* varNode = catchClause->left(); + MOZ_ASSERT_IF(varNode, catchClause->pn_pos.encloses(varNode->pn_pos)); + + ParseNode* bodyNode = catchClause->right(); + MOZ_ASSERT(catchClause->pn_pos.encloses(bodyNode->pn_pos)); + + RootedValue var(cx), body(cx); + if (!optPattern(varNode, &var)) { + return false; + } + + return statement(bodyNode, &body) && + builder.catchClause(var, body, &catchClause->pn_pos, dst); +} + +bool ASTSerializer::tryStatement(TryNode* tryNode, MutableHandleValue dst) { + ParseNode* bodyNode = tryNode->body(); + MOZ_ASSERT(tryNode->pn_pos.encloses(bodyNode->pn_pos)); + + LexicalScopeNode* catchNode = tryNode->catchScope(); + MOZ_ASSERT_IF(catchNode, tryNode->pn_pos.encloses(catchNode->pn_pos)); + + ParseNode* finallyNode = tryNode->finallyBlock(); + MOZ_ASSERT_IF(finallyNode, tryNode->pn_pos.encloses(finallyNode->pn_pos)); + + RootedValue body(cx); + if (!statement(bodyNode, &body)) { + return false; + } + + RootedValue handler(cx, NullValue()); + if (catchNode) { + LexicalScopeNode* catchScope = &catchNode->as<LexicalScopeNode>(); + if (!catchClause(&catchScope->scopeBody()->as<BinaryNode>(), &handler)) { + return false; + } + } + + RootedValue finally(cx); + return optStatement(finallyNode, &finally) && + builder.tryStatement(body, handler, finally, &tryNode->pn_pos, dst); +} + +bool ASTSerializer::forInit(ParseNode* pn, MutableHandleValue dst) { + if (!pn) { + dst.setMagic(JS_SERIALIZE_NO_NODE); + return true; + } + + bool lexical = pn->isKind(ParseNodeKind::LetDecl) || + pn->isKind(ParseNodeKind::ConstDecl); + return (lexical || pn->isKind(ParseNodeKind::VarStmt)) + ? variableDeclaration(&pn->as<ListNode>(), lexical, dst) + : expression(pn, dst); +} + +bool ASTSerializer::forOf(ForNode* loop, ParseNode* iterExpr, HandleValue var, + HandleValue stmt, MutableHandleValue dst) { + RootedValue expr(cx); + + return expression(iterExpr, &expr) && + builder.forOfStatement(var, expr, stmt, &loop->pn_pos, dst); +} + +bool ASTSerializer::forIn(ForNode* loop, ParseNode* iterExpr, HandleValue var, + HandleValue stmt, MutableHandleValue dst) { + RootedValue expr(cx); + + return expression(iterExpr, &expr) && + builder.forInStatement(var, expr, stmt, &loop->pn_pos, dst); +} + +bool ASTSerializer::classDefinition(ClassNode* pn, bool expr, + MutableHandleValue dst) { + RootedValue className(cx, MagicValue(JS_SERIALIZE_NO_NODE)); + RootedValue heritage(cx); + RootedValue classBody(cx); + + if (ClassNames* names = pn->names()) { + if (!identifier(names->innerBinding(), &className)) { + return false; + } + } + + return optExpression(pn->heritage(), &heritage) && + statement(pn->memberList(), &classBody) && + builder.classDefinition(expr, className, heritage, classBody, + &pn->pn_pos, dst); +} + +bool ASTSerializer::statement(ParseNode* pn, MutableHandleValue dst) { + AutoCheckRecursionLimit recursion(cx); + if (!recursion.check(cx)) { + return false; + } + + switch (pn->getKind()) { + case ParseNodeKind::Function: + case ParseNodeKind::VarStmt: + return declaration(pn, dst); + + case ParseNodeKind::LetDecl: + case ParseNodeKind::ConstDecl: + return declaration(pn, dst); + + case ParseNodeKind::ImportDecl: + return importDeclaration(&pn->as<BinaryNode>(), dst); + + case ParseNodeKind::ExportStmt: + case ParseNodeKind::ExportDefaultStmt: + case ParseNodeKind::ExportFromStmt: + return exportDeclaration(pn, dst); + + case ParseNodeKind::EmptyStmt: + return builder.emptyStatement(&pn->pn_pos, dst); + + case ParseNodeKind::ExpressionStmt: { + RootedValue expr(cx); + return expression(pn->as<UnaryNode>().kid(), &expr) && + builder.expressionStatement(expr, &pn->pn_pos, dst); + } + + case ParseNodeKind::LexicalScope: + pn = pn->as<LexicalScopeNode>().scopeBody(); + if (!pn->isKind(ParseNodeKind::StatementList)) { + return statement(pn, dst); + } + [[fallthrough]]; + + case ParseNodeKind::StatementList: + return blockStatement(&pn->as<ListNode>(), dst); + + case ParseNodeKind::IfStmt: { + TernaryNode* ifNode = &pn->as<TernaryNode>(); + + ParseNode* testNode = ifNode->kid1(); + MOZ_ASSERT(ifNode->pn_pos.encloses(testNode->pn_pos)); + + ParseNode* consNode = ifNode->kid2(); + MOZ_ASSERT(ifNode->pn_pos.encloses(consNode->pn_pos)); + + ParseNode* altNode = ifNode->kid3(); + MOZ_ASSERT_IF(altNode, ifNode->pn_pos.encloses(altNode->pn_pos)); + + RootedValue test(cx), cons(cx), alt(cx); + + return expression(testNode, &test) && statement(consNode, &cons) && + optStatement(altNode, &alt) && + builder.ifStatement(test, cons, alt, &ifNode->pn_pos, dst); + } + + case ParseNodeKind::SwitchStmt: + return switchStatement(&pn->as<SwitchStatement>(), dst); + + case ParseNodeKind::TryStmt: + return tryStatement(&pn->as<TryNode>(), dst); + + case ParseNodeKind::WithStmt: + case ParseNodeKind::WhileStmt: { + BinaryNode* node = &pn->as<BinaryNode>(); + + ParseNode* exprNode = node->left(); + MOZ_ASSERT(node->pn_pos.encloses(exprNode->pn_pos)); + + ParseNode* stmtNode = node->right(); + MOZ_ASSERT(node->pn_pos.encloses(stmtNode->pn_pos)); + + RootedValue expr(cx), stmt(cx); + + return expression(exprNode, &expr) && statement(stmtNode, &stmt) && + (node->isKind(ParseNodeKind::WithStmt) + ? builder.withStatement(expr, stmt, &node->pn_pos, dst) + : builder.whileStatement(expr, stmt, &node->pn_pos, dst)); + } + + case ParseNodeKind::DoWhileStmt: { + BinaryNode* node = &pn->as<BinaryNode>(); + + ParseNode* stmtNode = node->left(); + MOZ_ASSERT(node->pn_pos.encloses(stmtNode->pn_pos)); + + ParseNode* testNode = node->right(); + MOZ_ASSERT(node->pn_pos.encloses(testNode->pn_pos)); + + RootedValue stmt(cx), test(cx); + + return statement(stmtNode, &stmt) && expression(testNode, &test) && + builder.doWhileStatement(stmt, test, &node->pn_pos, dst); + } + + case ParseNodeKind::ForStmt: { + ForNode* forNode = &pn->as<ForNode>(); + + TernaryNode* head = forNode->head(); + MOZ_ASSERT(forNode->pn_pos.encloses(head->pn_pos)); + + ParseNode* stmtNode = forNode->right(); + MOZ_ASSERT(forNode->pn_pos.encloses(stmtNode->pn_pos)); + + ParseNode* initNode = head->kid1(); + MOZ_ASSERT_IF(initNode, head->pn_pos.encloses(initNode->pn_pos)); + + ParseNode* maybeTest = head->kid2(); + MOZ_ASSERT_IF(maybeTest, head->pn_pos.encloses(maybeTest->pn_pos)); + + ParseNode* updateOrIter = head->kid3(); + MOZ_ASSERT_IF(updateOrIter, head->pn_pos.encloses(updateOrIter->pn_pos)); + + RootedValue stmt(cx); + if (!statement(stmtNode, &stmt)) { + return false; + } + + if (head->isKind(ParseNodeKind::ForIn) || + head->isKind(ParseNodeKind::ForOf)) { + RootedValue var(cx); + if (initNode->is<LexicalScopeNode>()) { + LexicalScopeNode* scopeNode = &initNode->as<LexicalScopeNode>(); + if (!variableDeclaration(&scopeNode->scopeBody()->as<ListNode>(), + true, &var)) { + return false; + } + } else if (!initNode->isKind(ParseNodeKind::VarStmt) && + !initNode->isKind(ParseNodeKind::LetDecl) && + !initNode->isKind(ParseNodeKind::ConstDecl)) { + if (!pattern(initNode, &var)) { + return false; + } + } else { + if (!variableDeclaration( + &initNode->as<ListNode>(), + initNode->isKind(ParseNodeKind::LetDecl) || + initNode->isKind(ParseNodeKind::ConstDecl), + &var)) { + return false; + } + } + if (head->isKind(ParseNodeKind::ForIn)) { + return forIn(forNode, updateOrIter, var, stmt, dst); + } + return forOf(forNode, updateOrIter, var, stmt, dst); + } + + RootedValue init(cx), test(cx), update(cx); + + return forInit(initNode, &init) && optExpression(maybeTest, &test) && + optExpression(updateOrIter, &update) && + builder.forStatement(init, test, update, stmt, &forNode->pn_pos, + dst); + } + + case ParseNodeKind::BreakStmt: + case ParseNodeKind::ContinueStmt: { + LoopControlStatement* node = &pn->as<LoopControlStatement>(); + RootedValue label(cx); + Rooted<JSAtom*> pnAtom(cx); + if (node->label()) { + pnAtom.set(parser->liftParserAtomToJSAtom(node->label())); + if (!pnAtom) { + return false; + } + } + return optIdentifier(pnAtom, nullptr, &label) && + (node->isKind(ParseNodeKind::BreakStmt) + ? builder.breakStatement(label, &node->pn_pos, dst) + : builder.continueStatement(label, &node->pn_pos, dst)); + } + + case ParseNodeKind::LabelStmt: { + LabeledStatement* labelNode = &pn->as<LabeledStatement>(); + ParseNode* stmtNode = labelNode->statement(); + MOZ_ASSERT(labelNode->pn_pos.encloses(stmtNode->pn_pos)); + + RootedValue label(cx), stmt(cx); + Rooted<JSAtom*> pnAtom( + cx, parser->liftParserAtomToJSAtom(labelNode->label())); + if (!pnAtom.get()) { + return false; + } + return identifier(pnAtom, nullptr, &label) && + statement(stmtNode, &stmt) && + builder.labeledStatement(label, stmt, &labelNode->pn_pos, dst); + } + + case ParseNodeKind::ThrowStmt: { + UnaryNode* throwNode = &pn->as<UnaryNode>(); + ParseNode* operand = throwNode->kid(); + MOZ_ASSERT(throwNode->pn_pos.encloses(operand->pn_pos)); + + RootedValue arg(cx); + + return expression(operand, &arg) && + builder.throwStatement(arg, &throwNode->pn_pos, dst); + } + + case ParseNodeKind::ReturnStmt: { + UnaryNode* returnNode = &pn->as<UnaryNode>(); + ParseNode* operand = returnNode->kid(); + MOZ_ASSERT_IF(operand, returnNode->pn_pos.encloses(operand->pn_pos)); + + RootedValue arg(cx); + + return optExpression(operand, &arg) && + builder.returnStatement(arg, &returnNode->pn_pos, dst); + } + + case ParseNodeKind::DebuggerStmt: + return builder.debuggerStatement(&pn->pn_pos, dst); + + case ParseNodeKind::ClassDecl: + return classDefinition(&pn->as<ClassNode>(), false, dst); + + case ParseNodeKind::ClassMemberList: { + ListNode* memberList = &pn->as<ListNode>(); + NodeVector members(cx); + if (!members.reserve(memberList->count())) { + return false; + } + + for (ParseNode* item : memberList->contents()) { + if (item->is<LexicalScopeNode>()) { + item = item->as<LexicalScopeNode>().scopeBody(); + } + if (item->is<ClassField>()) { + ClassField* field = &item->as<ClassField>(); + MOZ_ASSERT(memberList->pn_pos.encloses(field->pn_pos)); + + RootedValue prop(cx); + if (!classField(field, &prop)) { + return false; + } + members.infallibleAppend(prop); + } else if (item->is<StaticClassBlock>()) { + // StaticClassBlock* block = &item->as<StaticClassBlock>(); + StaticClassBlock* scb = &item->as<StaticClassBlock>(); + MOZ_ASSERT(memberList->pn_pos.encloses(scb->pn_pos)); + RootedValue prop(cx); + if (!staticClassBlock(scb, &prop)) { + return false; + } + members.infallibleAppend(prop); + } else if (!item->isKind(ParseNodeKind::DefaultConstructor)) { + ClassMethod* method = &item->as<ClassMethod>(); + MOZ_ASSERT(memberList->pn_pos.encloses(method->pn_pos)); + + RootedValue prop(cx); + if (!classMethod(method, &prop)) { + return false; + } + members.infallibleAppend(prop); + } + } + + return builder.classMembers(members, dst); + } + + default: + LOCAL_NOT_REACHED("unexpected statement type"); + } +} + +bool ASTSerializer::classMethod(ClassMethod* classMethod, + MutableHandleValue dst) { + PropKind kind; + switch (classMethod->accessorType()) { + case AccessorType::None: + kind = PROP_INIT; + break; + + case AccessorType::Getter: + kind = PROP_GETTER; + break; + + case AccessorType::Setter: + kind = PROP_SETTER; + break; + + default: + LOCAL_NOT_REACHED("unexpected object-literal property"); + } + + RootedValue key(cx), val(cx); + bool isStatic = classMethod->isStatic(); + return propertyName(&classMethod->name(), &key) && + expression(&classMethod->method(), &val) && + builder.classMethod(key, val, kind, isStatic, &classMethod->pn_pos, + dst); +} + +bool ASTSerializer::classField(ClassField* classField, MutableHandleValue dst) { + RootedValue key(cx), val(cx); + // Dig through the lambda and get to the actual expression + ParseNode* value = classField->initializer() + ->body() + ->body() + ->scopeBody() + ->as<ListNode>() + .head() + ->as<UnaryNode>() + .kid() + ->as<BinaryNode>() + .right(); + // RawUndefinedExpr is the node we use for "there is no initializer". If one + // writes, literally, `x = undefined;`, it will not be a RawUndefinedExpr + // node, but rather a variable reference. + // Behavior for "there is no initializer" should be { ..., "init": null } + if (value->getKind() != ParseNodeKind::RawUndefinedExpr) { + if (!expression(value, &val)) { + return false; + } + } else { + val.setNull(); + } + return propertyName(&classField->name(), &key) && + builder.classField(key, val, &classField->pn_pos, dst); +} + +bool ASTSerializer::staticClassBlock(StaticClassBlock* staticClassBlock, + MutableHandleValue dst) { + FunctionNode* fun = staticClassBlock->function(); + + NodeVector args(cx); + NodeVector defaults(cx); + + RootedValue body(cx), rest(cx); + rest.setNull(); + return functionArgsAndBody(fun->body(), args, defaults, false, false, &body, + &rest) && + builder.staticClassBlock(body, &staticClassBlock->pn_pos, dst); +} + +bool ASTSerializer::leftAssociate(ListNode* node, MutableHandleValue dst) { + MOZ_ASSERT(!node->empty()); + + ParseNodeKind pnk = node->getKind(); + bool lor = pnk == ParseNodeKind::OrExpr; + bool coalesce = pnk == ParseNodeKind::CoalesceExpr; + bool logop = lor || coalesce || pnk == ParseNodeKind::AndExpr; + + ParseNode* head = node->head(); + RootedValue left(cx); + if (!expression(head, &left)) { + return false; + } + for (ParseNode* next : node->contentsFrom(head->pn_next)) { + RootedValue right(cx); + if (!expression(next, &right)) { + return false; + } + + TokenPos subpos(node->pn_pos.begin, next->pn_pos.end); + + if (logop) { + if (!builder.logicalExpression(pnk, left, right, &subpos, &left)) { + return false; + } + } else { + BinaryOperator op = binop(node->getKind()); + LOCAL_ASSERT(op > BINOP_ERR && op < BINOP_LIMIT); + + if (!builder.binaryExpression(op, left, right, &subpos, &left)) { + return false; + } + } + } + + dst.set(left); + return true; +} + +bool ASTSerializer::rightAssociate(ListNode* node, MutableHandleValue dst) { + MOZ_ASSERT(!node->empty()); + + // First, we need to reverse the list, so that we can traverse it in the right + // order. It's OK to destructively reverse the list, because there are no + // other consumers. + + ParseNode* head = node->head(); + ParseNode* prev = nullptr; + ParseNode* current = head; + ParseNode* next; + while (current != nullptr) { + next = current->pn_next; + current->pn_next = prev; + prev = current; + current = next; + } + + head = prev; + + RootedValue right(cx); + if (!expression(head, &right)) { + return false; + } + for (ParseNode* next = head->pn_next; next; next = next->pn_next) { + RootedValue left(cx); + if (!expression(next, &left)) { + return false; + } + + TokenPos subpos(node->pn_pos.begin, next->pn_pos.end); + + BinaryOperator op = binop(node->getKind()); + LOCAL_ASSERT(op > BINOP_ERR && op < BINOP_LIMIT); + + if (!builder.binaryExpression(op, left, right, &subpos, &right)) { + return false; + } + } + + dst.set(right); + return true; +} + +bool ASTSerializer::expression(ParseNode* pn, MutableHandleValue dst) { + AutoCheckRecursionLimit recursion(cx); + if (!recursion.check(cx)) { + return false; + } + + switch (pn->getKind()) { + case ParseNodeKind::Function: { + FunctionNode* funNode = &pn->as<FunctionNode>(); + ASTType type = + funNode->funbox()->isArrow() ? AST_ARROW_EXPR : AST_FUNC_EXPR; + return function(funNode, type, dst); + } + + case ParseNodeKind::CommaExpr: { + NodeVector exprs(cx); + return expressions(&pn->as<ListNode>(), exprs) && + builder.sequenceExpression(exprs, &pn->pn_pos, dst); + } + + case ParseNodeKind::ConditionalExpr: { + ConditionalExpression* condNode = &pn->as<ConditionalExpression>(); + ParseNode* testNode = condNode->kid1(); + ParseNode* consNode = condNode->kid2(); + ParseNode* altNode = condNode->kid3(); + MOZ_ASSERT(condNode->pn_pos.encloses(testNode->pn_pos)); + MOZ_ASSERT(condNode->pn_pos.encloses(consNode->pn_pos)); + MOZ_ASSERT(condNode->pn_pos.encloses(altNode->pn_pos)); + + RootedValue test(cx), cons(cx), alt(cx); + + return expression(testNode, &test) && expression(consNode, &cons) && + expression(altNode, &alt) && + builder.conditionalExpression(test, cons, alt, &condNode->pn_pos, + dst); + } + + case ParseNodeKind::CoalesceExpr: + case ParseNodeKind::OrExpr: + case ParseNodeKind::AndExpr: + return leftAssociate(&pn->as<ListNode>(), dst); + + case ParseNodeKind::PreIncrementExpr: + case ParseNodeKind::PreDecrementExpr: { + UnaryNode* incDec = &pn->as<UnaryNode>(); + ParseNode* operand = incDec->kid(); + MOZ_ASSERT(incDec->pn_pos.encloses(operand->pn_pos)); + + bool inc = incDec->isKind(ParseNodeKind::PreIncrementExpr); + RootedValue expr(cx); + return expression(operand, &expr) && + builder.updateExpression(expr, inc, true, &incDec->pn_pos, dst); + } + + case ParseNodeKind::PostIncrementExpr: + case ParseNodeKind::PostDecrementExpr: { + UnaryNode* incDec = &pn->as<UnaryNode>(); + ParseNode* operand = incDec->kid(); + MOZ_ASSERT(incDec->pn_pos.encloses(operand->pn_pos)); + + bool inc = incDec->isKind(ParseNodeKind::PostIncrementExpr); + RootedValue expr(cx); + return expression(operand, &expr) && + builder.updateExpression(expr, inc, false, &incDec->pn_pos, dst); + } + + case ParseNodeKind::AssignExpr: + case ParseNodeKind::AddAssignExpr: + case ParseNodeKind::SubAssignExpr: + case ParseNodeKind::CoalesceAssignExpr: + case ParseNodeKind::OrAssignExpr: + case ParseNodeKind::AndAssignExpr: + case ParseNodeKind::BitOrAssignExpr: + case ParseNodeKind::BitXorAssignExpr: + case ParseNodeKind::BitAndAssignExpr: + case ParseNodeKind::LshAssignExpr: + case ParseNodeKind::RshAssignExpr: + case ParseNodeKind::UrshAssignExpr: + case ParseNodeKind::MulAssignExpr: + case ParseNodeKind::DivAssignExpr: + case ParseNodeKind::ModAssignExpr: + case ParseNodeKind::PowAssignExpr: { + AssignmentNode* assignNode = &pn->as<AssignmentNode>(); + ParseNode* lhsNode = assignNode->left(); + ParseNode* rhsNode = assignNode->right(); + MOZ_ASSERT(assignNode->pn_pos.encloses(lhsNode->pn_pos)); + MOZ_ASSERT(assignNode->pn_pos.encloses(rhsNode->pn_pos)); + + AssignmentOperator op = aop(assignNode->getKind()); + LOCAL_ASSERT(op > AOP_ERR && op < AOP_LIMIT); + + RootedValue lhs(cx), rhs(cx); + return pattern(lhsNode, &lhs) && expression(rhsNode, &rhs) && + builder.assignmentExpression(op, lhs, rhs, &assignNode->pn_pos, + dst); + } + + case ParseNodeKind::AddExpr: + case ParseNodeKind::SubExpr: + case ParseNodeKind::StrictEqExpr: + case ParseNodeKind::EqExpr: + case ParseNodeKind::StrictNeExpr: + case ParseNodeKind::NeExpr: + case ParseNodeKind::LtExpr: + case ParseNodeKind::LeExpr: + case ParseNodeKind::GtExpr: + case ParseNodeKind::GeExpr: + case ParseNodeKind::LshExpr: + case ParseNodeKind::RshExpr: + case ParseNodeKind::UrshExpr: + case ParseNodeKind::MulExpr: + case ParseNodeKind::DivExpr: + case ParseNodeKind::ModExpr: + case ParseNodeKind::BitOrExpr: + case ParseNodeKind::BitXorExpr: + case ParseNodeKind::BitAndExpr: + case ParseNodeKind::InExpr: + case ParseNodeKind::PrivateInExpr: + case ParseNodeKind::InstanceOfExpr: + return leftAssociate(&pn->as<ListNode>(), dst); + + case ParseNodeKind::PowExpr: + return rightAssociate(&pn->as<ListNode>(), dst); + + case ParseNodeKind::DeleteNameExpr: + case ParseNodeKind::DeletePropExpr: + case ParseNodeKind::DeleteElemExpr: + case ParseNodeKind::DeleteExpr: + case ParseNodeKind::TypeOfNameExpr: + case ParseNodeKind::TypeOfExpr: + case ParseNodeKind::VoidExpr: + case ParseNodeKind::NotExpr: + case ParseNodeKind::BitNotExpr: + case ParseNodeKind::PosExpr: + case ParseNodeKind::AwaitExpr: + case ParseNodeKind::NegExpr: { + UnaryNode* unaryNode = &pn->as<UnaryNode>(); + ParseNode* operand = unaryNode->kid(); + MOZ_ASSERT(unaryNode->pn_pos.encloses(operand->pn_pos)); + + UnaryOperator op = unop(unaryNode->getKind()); + LOCAL_ASSERT(op > UNOP_ERR && op < UNOP_LIMIT); + + RootedValue expr(cx); + return expression(operand, &expr) && + builder.unaryExpression(op, expr, &unaryNode->pn_pos, dst); + } + + case ParseNodeKind::DeleteOptionalChainExpr: { + RootedValue expr(cx); + return expression(pn->as<UnaryNode>().kid(), &expr) && + builder.deleteOptionalExpression(expr, &pn->pn_pos, dst); + } + + case ParseNodeKind::OptionalChain: { + RootedValue expr(cx); + return expression(pn->as<UnaryNode>().kid(), &expr) && + builder.optionalExpression(expr, &pn->pn_pos, dst); + } + + case ParseNodeKind::NewExpr: + case ParseNodeKind::TaggedTemplateExpr: + case ParseNodeKind::CallExpr: + case ParseNodeKind::OptionalCallExpr: + case ParseNodeKind::SuperCallExpr: { + BinaryNode* node = &pn->as<BinaryNode>(); + ParseNode* calleeNode = node->left(); + ListNode* argsList = &node->right()->as<ListNode>(); + MOZ_ASSERT(node->pn_pos.encloses(calleeNode->pn_pos)); + + RootedValue callee(cx); + if (node->isKind(ParseNodeKind::SuperCallExpr)) { + MOZ_ASSERT(calleeNode->isKind(ParseNodeKind::SuperBase)); + if (!builder.super(&calleeNode->pn_pos, &callee)) { + return false; + } + } else { + if (!expression(calleeNode, &callee)) { + return false; + } + } + + NodeVector args(cx); + if (!args.reserve(argsList->count())) { + return false; + } + + for (ParseNode* argNode : argsList->contents()) { + MOZ_ASSERT(node->pn_pos.encloses(argNode->pn_pos)); + + RootedValue arg(cx); + if (!expression(argNode, &arg)) { + return false; + } + args.infallibleAppend(arg); + } + + if (node->getKind() == ParseNodeKind::TaggedTemplateExpr) { + return builder.taggedTemplate(callee, args, &node->pn_pos, dst); + } + + bool isOptional = node->isKind(ParseNodeKind::OptionalCallExpr); + + // SUPERCALL is Call(super, args) + return node->isKind(ParseNodeKind::NewExpr) + ? builder.newExpression(callee, args, &node->pn_pos, dst) + : builder.callExpression(callee, args, &node->pn_pos, dst, + isOptional); + } + + case ParseNodeKind::DotExpr: + case ParseNodeKind::OptionalDotExpr: { + PropertyAccessBase* prop = &pn->as<PropertyAccessBase>(); + MOZ_ASSERT(prop->pn_pos.encloses(prop->expression().pn_pos)); + + bool isSuper = + prop->is<PropertyAccess>() && prop->as<PropertyAccess>().isSuper(); + + RootedValue expr(cx); + RootedValue propname(cx); + Rooted<JSAtom*> pnAtom( + cx, parser->liftParserAtomToJSAtom(prop->key().atom())); + if (!pnAtom.get()) { + return false; + } + + if (isSuper) { + if (!builder.super(&prop->expression().pn_pos, &expr)) { + return false; + } + } else { + if (!expression(&prop->expression(), &expr)) { + return false; + } + } + + bool isOptional = prop->isKind(ParseNodeKind::OptionalDotExpr); + + return identifier(pnAtom, nullptr, &propname) && + builder.memberExpression(false, expr, propname, &prop->pn_pos, dst, + isOptional); + } + + case ParseNodeKind::ElemExpr: + case ParseNodeKind::OptionalElemExpr: { + PropertyByValueBase* elem = &pn->as<PropertyByValueBase>(); + MOZ_ASSERT(elem->pn_pos.encloses(elem->expression().pn_pos)); + MOZ_ASSERT(elem->pn_pos.encloses(elem->key().pn_pos)); + + bool isSuper = + elem->is<PropertyByValue>() && elem->as<PropertyByValue>().isSuper(); + + RootedValue expr(cx), key(cx); + + if (isSuper) { + if (!builder.super(&elem->expression().pn_pos, &expr)) { + return false; + } + } else { + if (!expression(&elem->expression(), &expr)) { + return false; + } + } + + bool isOptional = elem->isKind(ParseNodeKind::OptionalElemExpr); + + return expression(&elem->key(), &key) && + builder.memberExpression(true, expr, key, &elem->pn_pos, dst, + isOptional); + } + + case ParseNodeKind::PrivateMemberExpr: + case ParseNodeKind::OptionalPrivateMemberExpr: { + PrivateMemberAccessBase* privateExpr = &pn->as<PrivateMemberAccessBase>(); + MOZ_ASSERT( + privateExpr->pn_pos.encloses(privateExpr->expression().pn_pos)); + MOZ_ASSERT( + privateExpr->pn_pos.encloses(privateExpr->privateName().pn_pos)); + + RootedValue expr(cx), key(cx); + + if (!expression(&privateExpr->expression(), &expr)) { + return false; + } + + bool isOptional = + privateExpr->isKind(ParseNodeKind::OptionalPrivateMemberExpr); + + return expression(&privateExpr->privateName(), &key) && + builder.memberExpression(true, expr, key, &privateExpr->pn_pos, + dst, isOptional); + } + + case ParseNodeKind::CallSiteObj: { + CallSiteNode* callSiteObj = &pn->as<CallSiteNode>(); + ListNode* rawNodes = callSiteObj->rawNodes(); + NodeVector raw(cx); + if (!raw.reserve(rawNodes->count())) { + return false; + } + for (ParseNode* item : rawNodes->contents()) { + NameNode* rawItem = &item->as<NameNode>(); + MOZ_ASSERT(callSiteObj->pn_pos.encloses(rawItem->pn_pos)); + + JSAtom* exprAtom = parser->liftParserAtomToJSAtom(rawItem->atom()); + if (!exprAtom) { + return false; + } + RootedValue expr(cx, StringValue(exprAtom)); + raw.infallibleAppend(expr); + } + + NodeVector cooked(cx); + if (!cooked.reserve(callSiteObj->count() - 1)) { + return false; + } + + for (ParseNode* cookedItem : + callSiteObj->contentsFrom(rawNodes->pn_next)) { + MOZ_ASSERT(callSiteObj->pn_pos.encloses(cookedItem->pn_pos)); + + RootedValue expr(cx); + if (cookedItem->isKind(ParseNodeKind::RawUndefinedExpr)) { + expr.setUndefined(); + } else { + MOZ_ASSERT(cookedItem->isKind(ParseNodeKind::TemplateStringExpr)); + JSAtom* exprAtom = + parser->liftParserAtomToJSAtom(cookedItem->as<NameNode>().atom()); + if (!exprAtom) { + return false; + } + expr.setString(exprAtom); + } + cooked.infallibleAppend(expr); + } + + return builder.callSiteObj(raw, cooked, &callSiteObj->pn_pos, dst); + } + + case ParseNodeKind::ArrayExpr: { + ListNode* array = &pn->as<ListNode>(); + NodeVector elts(cx); + if (!elts.reserve(array->count())) { + return false; + } + + for (ParseNode* item : array->contents()) { + MOZ_ASSERT(array->pn_pos.encloses(item->pn_pos)); + + if (item->isKind(ParseNodeKind::Elision)) { + elts.infallibleAppend(NullValue()); + } else { + RootedValue expr(cx); + if (!expression(item, &expr)) { + return false; + } + elts.infallibleAppend(expr); + } + } + + return builder.arrayExpression(elts, &array->pn_pos, dst); + } + + case ParseNodeKind::Spread: { + RootedValue expr(cx); + return expression(pn->as<UnaryNode>().kid(), &expr) && + builder.spreadExpression(expr, &pn->pn_pos, dst); + } + + case ParseNodeKind::ComputedName: { + if (pn->as<UnaryNode>().isSyntheticComputedName()) { + return literal(pn->as<UnaryNode>().kid(), dst); + } + RootedValue name(cx); + return expression(pn->as<UnaryNode>().kid(), &name) && + builder.computedName(name, &pn->pn_pos, dst); + } + + case ParseNodeKind::ObjectExpr: { + ListNode* obj = &pn->as<ListNode>(); + NodeVector elts(cx); + if (!elts.reserve(obj->count())) { + return false; + } + + for (ParseNode* item : obj->contents()) { + MOZ_ASSERT(obj->pn_pos.encloses(item->pn_pos)); + + RootedValue prop(cx); + if (!property(item, &prop)) { + return false; + } + elts.infallibleAppend(prop); + } + + return builder.objectExpression(elts, &obj->pn_pos, dst); + } + +#ifdef ENABLE_RECORD_TUPLE + case ParseNodeKind::RecordExpr: { + ListNode* record = &pn->as<ListNode>(); + NodeVector elts(cx); + if (!elts.reserve(record->count())) { + return false; + } + + for (ParseNode* item : record->contents()) { + MOZ_ASSERT(record->pn_pos.encloses(item->pn_pos)); + + RootedValue prop(cx); + if (!property(item, &prop)) { + return false; + } + elts.infallibleAppend(prop); + } + + return builder.recordExpression(elts, &record->pn_pos, dst); + } + + case ParseNodeKind::TupleExpr: { + ListNode* tuple = &pn->as<ListNode>(); + NodeVector elts(cx); + if (!elts.reserve(tuple->count())) { + return false; + } + + for (ParseNode* item : tuple->contents()) { + MOZ_ASSERT(tuple->pn_pos.encloses(item->pn_pos)); + + RootedValue expr(cx); + if (!expression(item, &expr)) { + return false; + } + elts.infallibleAppend(expr); + } + + return builder.tupleExpression(elts, &tuple->pn_pos, dst); + } +#endif + + case ParseNodeKind::PrivateName: + case ParseNodeKind::Name: + return identifier(&pn->as<NameNode>(), dst); + + case ParseNodeKind::ThisExpr: + return builder.thisExpression(&pn->pn_pos, dst); + + case ParseNodeKind::TemplateStringListExpr: { + ListNode* list = &pn->as<ListNode>(); + NodeVector elts(cx); + if (!elts.reserve(list->count())) { + return false; + } + + for (ParseNode* item : list->contents()) { + MOZ_ASSERT(list->pn_pos.encloses(item->pn_pos)); + + RootedValue expr(cx); + if (!expression(item, &expr)) { + return false; + } + elts.infallibleAppend(expr); + } + + return builder.templateLiteral(elts, &list->pn_pos, dst); + } + + case ParseNodeKind::TemplateStringExpr: + case ParseNodeKind::StringExpr: + case ParseNodeKind::RegExpExpr: + case ParseNodeKind::NumberExpr: + case ParseNodeKind::BigIntExpr: + case ParseNodeKind::TrueExpr: + case ParseNodeKind::FalseExpr: + case ParseNodeKind::NullExpr: + case ParseNodeKind::RawUndefinedExpr: + return literal(pn, dst); + + case ParseNodeKind::YieldStarExpr: { + UnaryNode* yieldNode = &pn->as<UnaryNode>(); + ParseNode* operand = yieldNode->kid(); + MOZ_ASSERT(yieldNode->pn_pos.encloses(operand->pn_pos)); + + RootedValue arg(cx); + return expression(operand, &arg) && + builder.yieldExpression(arg, Delegating, &yieldNode->pn_pos, dst); + } + + case ParseNodeKind::YieldExpr: { + UnaryNode* yieldNode = &pn->as<UnaryNode>(); + ParseNode* operand = yieldNode->kid(); + MOZ_ASSERT_IF(operand, yieldNode->pn_pos.encloses(operand->pn_pos)); + + RootedValue arg(cx); + return optExpression(operand, &arg) && + builder.yieldExpression(arg, NotDelegating, &yieldNode->pn_pos, + dst); + } + + case ParseNodeKind::ClassDecl: + return classDefinition(&pn->as<ClassNode>(), true, dst); + + case ParseNodeKind::NewTargetExpr: { + auto* node = &pn->as<NewTargetNode>(); + ParseNode* firstNode = node->newHolder(); + MOZ_ASSERT(firstNode->isKind(ParseNodeKind::PosHolder)); + MOZ_ASSERT(node->pn_pos.encloses(firstNode->pn_pos)); + + ParseNode* secondNode = node->targetHolder(); + MOZ_ASSERT(secondNode->isKind(ParseNodeKind::PosHolder)); + MOZ_ASSERT(node->pn_pos.encloses(secondNode->pn_pos)); + + RootedValue firstIdent(cx); + RootedValue secondIdent(cx); + + Rooted<JSAtom*> firstStr(cx, cx->names().new_); + Rooted<JSAtom*> secondStr(cx, cx->names().target); + + return identifier(firstStr, &firstNode->pn_pos, &firstIdent) && + identifier(secondStr, &secondNode->pn_pos, &secondIdent) && + builder.metaProperty(firstIdent, secondIdent, &node->pn_pos, dst); + } + + case ParseNodeKind::ImportMetaExpr: { + BinaryNode* node = &pn->as<BinaryNode>(); + ParseNode* firstNode = node->left(); + MOZ_ASSERT(firstNode->isKind(ParseNodeKind::PosHolder)); + MOZ_ASSERT(node->pn_pos.encloses(firstNode->pn_pos)); + + ParseNode* secondNode = node->right(); + MOZ_ASSERT(secondNode->isKind(ParseNodeKind::PosHolder)); + MOZ_ASSERT(node->pn_pos.encloses(secondNode->pn_pos)); + + RootedValue firstIdent(cx); + RootedValue secondIdent(cx); + + Rooted<JSAtom*> firstStr(cx, cx->names().import); + Rooted<JSAtom*> secondStr(cx, cx->names().meta); + + return identifier(firstStr, &firstNode->pn_pos, &firstIdent) && + identifier(secondStr, &secondNode->pn_pos, &secondIdent) && + builder.metaProperty(firstIdent, secondIdent, &node->pn_pos, dst); + } + + case ParseNodeKind::CallImportExpr: { + BinaryNode* node = &pn->as<BinaryNode>(); + ParseNode* identNode = node->left(); + MOZ_ASSERT(identNode->isKind(ParseNodeKind::PosHolder)); + MOZ_ASSERT(identNode->pn_pos.encloses(identNode->pn_pos)); + + ParseNode* specNode = node->right(); + MOZ_ASSERT(specNode->is<BinaryNode>()); + MOZ_ASSERT(specNode->isKind(ParseNodeKind::CallImportSpec)); + + ParseNode* argNode = specNode->as<BinaryNode>().left(); + MOZ_ASSERT(node->pn_pos.encloses(argNode->pn_pos)); + + ParseNode* optionsArgNode = specNode->as<BinaryNode>().right(); + MOZ_ASSERT(node->pn_pos.encloses(optionsArgNode->pn_pos)); + + RootedValue ident(cx); + Handle<PropertyName*> name = cx->names().import; + if (!identifier(name, &identNode->pn_pos, &ident)) { + return false; + } + + NodeVector args(cx); + + RootedValue arg(cx); + if (!expression(argNode, &arg)) { + return false; + } + if (!args.append(arg)) { + return false; + } + + if (!optionsArgNode->isKind(ParseNodeKind::PosHolder)) { + RootedValue optionsArg(cx); + if (!expression(optionsArgNode, &optionsArg)) { + return false; + } + if (!args.append(optionsArg)) { + return false; + } + } + + return builder.callImportExpression(ident, args, &pn->pn_pos, dst); + } + + case ParseNodeKind::SetThis: { + // SETTHIS is used to assign the result of a super() call to |this|. + // It's not part of the original AST, so just forward to the call. + BinaryNode* node = &pn->as<BinaryNode>(); + MOZ_ASSERT(node->left()->isKind(ParseNodeKind::Name)); + return expression(node->right(), dst); + } + + default: + LOCAL_NOT_REACHED("unexpected expression type"); + } +} + +bool ASTSerializer::propertyName(ParseNode* key, MutableHandleValue dst) { + if (key->isKind(ParseNodeKind::ComputedName)) { + return expression(key, dst); + } + if (key->isKind(ParseNodeKind::ObjectPropertyName) || + key->isKind(ParseNodeKind::PrivateName)) { + return identifier(&key->as<NameNode>(), dst); + } + + LOCAL_ASSERT(key->isKind(ParseNodeKind::StringExpr) || + key->isKind(ParseNodeKind::NumberExpr) || + key->isKind(ParseNodeKind::BigIntExpr)); + + return literal(key, dst); +} + +bool ASTSerializer::property(ParseNode* pn, MutableHandleValue dst) { + if (pn->isKind(ParseNodeKind::MutateProto)) { + RootedValue val(cx); + return expression(pn->as<UnaryNode>().kid(), &val) && + builder.prototypeMutation(val, &pn->pn_pos, dst); + } + if (pn->isKind(ParseNodeKind::Spread)) { + return expression(pn, dst); + } + + PropKind kind; + if (pn->is<PropertyDefinition>()) { + switch (pn->as<PropertyDefinition>().accessorType()) { + case AccessorType::None: + kind = PROP_INIT; + break; + + case AccessorType::Getter: + kind = PROP_GETTER; + break; + + case AccessorType::Setter: + kind = PROP_SETTER; + break; + + default: + LOCAL_NOT_REACHED("unexpected object-literal property"); + } + } else { + MOZ_ASSERT(pn->isKind(ParseNodeKind::Shorthand)); + kind = PROP_INIT; + } + + BinaryNode* node = &pn->as<BinaryNode>(); + ParseNode* keyNode = node->left(); + ParseNode* valNode = node->right(); + + bool isShorthand = node->isKind(ParseNodeKind::Shorthand); + bool isMethod = + valNode->is<FunctionNode>() && + valNode->as<FunctionNode>().funbox()->kind() == FunctionFlags::Method; + RootedValue key(cx), val(cx); + return propertyName(keyNode, &key) && expression(valNode, &val) && + builder.propertyInitializer(key, val, kind, isShorthand, isMethod, + &node->pn_pos, dst); +} + +bool ASTSerializer::literal(ParseNode* pn, MutableHandleValue dst) { + RootedValue val(cx); + switch (pn->getKind()) { + case ParseNodeKind::TemplateStringExpr: + case ParseNodeKind::StringExpr: { + JSAtom* exprAtom = + parser->liftParserAtomToJSAtom(pn->as<NameNode>().atom()); + if (!exprAtom) { + return false; + } + val.setString(exprAtom); + break; + } + + case ParseNodeKind::RegExpExpr: { + RegExpObject* re = pn->as<RegExpLiteral>().create( + cx, fc, parser->parserAtoms(), + parser->getCompilationState().input.atomCache, + parser->getCompilationState()); + if (!re) { + return false; + } + + val.setObject(*re); + break; + } + + case ParseNodeKind::NumberExpr: + val.setNumber(pn->as<NumericLiteral>().value()); + break; + + case ParseNodeKind::BigIntExpr: { + auto index = pn->as<BigIntLiteral>().index(); + BigInt* x = parser->compilationState_.bigIntData[index].createBigInt(cx); + if (!x) { + return false; + } + cx->check(x); + val.setBigInt(x); + break; + } + + case ParseNodeKind::NullExpr: + val.setNull(); + break; + + case ParseNodeKind::RawUndefinedExpr: + val.setUndefined(); + break; + + case ParseNodeKind::TrueExpr: + val.setBoolean(true); + break; + + case ParseNodeKind::FalseExpr: + val.setBoolean(false); + break; + + default: + LOCAL_NOT_REACHED("unexpected literal type"); + } + + return builder.literal(val, &pn->pn_pos, dst); +} + +bool ASTSerializer::arrayPattern(ListNode* array, MutableHandleValue dst) { + MOZ_ASSERT(array->isKind(ParseNodeKind::ArrayExpr)); + + NodeVector elts(cx); + if (!elts.reserve(array->count())) { + return false; + } + + for (ParseNode* item : array->contents()) { + if (item->isKind(ParseNodeKind::Elision)) { + elts.infallibleAppend(NullValue()); + } else if (item->isKind(ParseNodeKind::Spread)) { + RootedValue target(cx); + RootedValue spread(cx); + if (!pattern(item->as<UnaryNode>().kid(), &target)) { + return false; + } + if (!builder.spreadExpression(target, &item->pn_pos, &spread)) + return false; + elts.infallibleAppend(spread); + } else { + RootedValue patt(cx); + if (!pattern(item, &patt)) { + return false; + } + elts.infallibleAppend(patt); + } + } + + return builder.arrayPattern(elts, &array->pn_pos, dst); +} + +bool ASTSerializer::objectPattern(ListNode* obj, MutableHandleValue dst) { + MOZ_ASSERT(obj->isKind(ParseNodeKind::ObjectExpr)); + + NodeVector elts(cx); + if (!elts.reserve(obj->count())) { + return false; + } + + for (ParseNode* propdef : obj->contents()) { + if (propdef->isKind(ParseNodeKind::Spread)) { + RootedValue target(cx); + RootedValue spread(cx); + if (!pattern(propdef->as<UnaryNode>().kid(), &target)) { + return false; + } + if (!builder.spreadExpression(target, &propdef->pn_pos, &spread)) + return false; + elts.infallibleAppend(spread); + continue; + } + // Patterns can't have getters/setters. + LOCAL_ASSERT(!propdef->is<PropertyDefinition>() || + propdef->as<PropertyDefinition>().accessorType() == + AccessorType::None); + + RootedValue key(cx); + ParseNode* target; + if (propdef->isKind(ParseNodeKind::MutateProto)) { + RootedValue pname(cx, StringValue(cx->names().proto)); + if (!builder.literal(pname, &propdef->pn_pos, &key)) { + return false; + } + target = propdef->as<UnaryNode>().kid(); + } else { + BinaryNode* prop = &propdef->as<BinaryNode>(); + if (!propertyName(prop->left(), &key)) { + return false; + } + target = prop->right(); + } + + RootedValue patt(cx), prop(cx); + if (!pattern(target, &patt) || + !builder.propertyPattern(key, patt, + propdef->isKind(ParseNodeKind::Shorthand), + &propdef->pn_pos, &prop)) { + return false; + } + + elts.infallibleAppend(prop); + } + + return builder.objectPattern(elts, &obj->pn_pos, dst); +} + +bool ASTSerializer::pattern(ParseNode* pn, MutableHandleValue dst) { + AutoCheckRecursionLimit recursion(cx); + if (!recursion.check(cx)) { + return false; + } + + switch (pn->getKind()) { + case ParseNodeKind::ObjectExpr: + return objectPattern(&pn->as<ListNode>(), dst); + + case ParseNodeKind::ArrayExpr: + return arrayPattern(&pn->as<ListNode>(), dst); + + default: + return expression(pn, dst); + } +} + +bool ASTSerializer::identifier(Handle<JSAtom*> atom, TokenPos* pos, + MutableHandleValue dst) { + RootedValue atomContentsVal(cx, unrootedAtomContents(atom)); + return builder.identifier(atomContentsVal, pos, dst); +} + +bool ASTSerializer::identifier(NameNode* id, MutableHandleValue dst) { + LOCAL_ASSERT(id->atom()); + + Rooted<JSAtom*> pnAtom(cx, parser->liftParserAtomToJSAtom(id->atom())); + if (!pnAtom.get()) { + return false; + } + return identifier(pnAtom, &id->pn_pos, dst); +} + +bool ASTSerializer::identifierOrLiteral(ParseNode* id, MutableHandleValue dst) { + if (id->getKind() == ParseNodeKind::Name) { + return identifier(&id->as<NameNode>(), dst); + } + return literal(id, dst); +} + +bool ASTSerializer::function(FunctionNode* funNode, ASTType type, + MutableHandleValue dst) { + FunctionBox* funbox = funNode->funbox(); + + GeneratorStyle generatorStyle = + funbox->isGenerator() ? GeneratorStyle::ES6 : GeneratorStyle::None; + + bool isAsync = funbox->isAsync(); + bool isExpression = funbox->hasExprBody(); + + RootedValue id(cx); + Rooted<JSAtom*> funcAtom(cx); + if (funbox->explicitName()) { + funcAtom.set(parser->liftParserAtomToJSAtom(funbox->explicitName())); + if (!funcAtom) { + return false; + } + } + if (!optIdentifier(funcAtom, nullptr, &id)) { + return false; + } + + NodeVector args(cx); + NodeVector defaults(cx); + + RootedValue body(cx), rest(cx); + if (funbox->hasRest()) { + rest.setUndefined(); + } else { + rest.setNull(); + } + return functionArgsAndBody(funNode->body(), args, defaults, isAsync, + isExpression, &body, &rest) && + builder.function(type, &funNode->pn_pos, id, args, defaults, body, + rest, generatorStyle, isAsync, isExpression, dst); +} + +bool ASTSerializer::functionArgsAndBody(ParamsBodyNode* pn, NodeVector& args, + NodeVector& defaults, bool isAsync, + bool isExpression, + MutableHandleValue body, + MutableHandleValue rest) { + // Serialize the arguments. + if (!functionArgs(pn, args, defaults, rest)) { + return false; + } + + // Skip the enclosing lexical scope. + ParseNode* bodyNode = pn->body()->scopeBody(); + + // Serialize the body. + switch (bodyNode->getKind()) { + // Arrow function with expression body. + case ParseNodeKind::ReturnStmt: + MOZ_ASSERT(isExpression); + return expression(bodyNode->as<UnaryNode>().kid(), body); + + // Function with statement body. + case ParseNodeKind::StatementList: { + ParseNode* firstNode = bodyNode->as<ListNode>().head(); + + // Skip over initial yield in generator. + if (firstNode && firstNode->isKind(ParseNodeKind::InitialYield)) { + firstNode = firstNode->pn_next; + } + + // Async arrow with expression body is converted into STATEMENTLIST + // to insert initial yield. + if (isAsync && isExpression) { + MOZ_ASSERT(firstNode->getKind() == ParseNodeKind::ReturnStmt); + return expression(firstNode->as<UnaryNode>().kid(), body); + } + + return functionBody(firstNode, &bodyNode->pn_pos, body); + } + + default: + LOCAL_NOT_REACHED("unexpected function contents"); + } +} + +bool ASTSerializer::functionArgs(ParamsBodyNode* pn, NodeVector& args, + NodeVector& defaults, + MutableHandleValue rest) { + RootedValue node(cx); + bool defaultsNull = true; + MOZ_ASSERT(defaults.empty(), + "must be initially empty for it to be proper to clear this " + "when there are no defaults"); + + MOZ_ASSERT(rest.isNullOrUndefined(), + "rest is set to |undefined| when a rest argument is present, " + "otherwise rest is set to |null|"); + + for (ParseNode* arg : pn->parameters()) { + ParseNode* pat; + ParseNode* defNode; + if (arg->isKind(ParseNodeKind::Name) || + arg->isKind(ParseNodeKind::ArrayExpr) || + arg->isKind(ParseNodeKind::ObjectExpr)) { + pat = arg; + defNode = nullptr; + } else { + MOZ_ASSERT(arg->isKind(ParseNodeKind::AssignExpr)); + AssignmentNode* assignNode = &arg->as<AssignmentNode>(); + pat = assignNode->left(); + defNode = assignNode->right(); + } + + // Process the name or pattern. + MOZ_ASSERT(pat->isKind(ParseNodeKind::Name) || + pat->isKind(ParseNodeKind::ArrayExpr) || + pat->isKind(ParseNodeKind::ObjectExpr)); + if (!pattern(pat, &node)) { + return false; + } + if (rest.isUndefined() && arg->pn_next == *std::end(pn->parameters())) { + rest.setObject(node.toObject()); + } else { + if (!args.append(node)) { + return false; + } + } + + // Process its default (or lack thereof). + if (defNode) { + defaultsNull = false; + RootedValue def(cx); + if (!expression(defNode, &def) || !defaults.append(def)) { + return false; + } + } else { + if (!defaults.append(NullValue())) { + return false; + } + } + } + MOZ_ASSERT(!rest.isUndefined(), + "if a rest argument was present (signified by " + "|rest.isUndefined()| initially), the rest node was properly " + "recorded"); + + if (defaultsNull) { + defaults.clear(); + } + + return true; +} + +bool ASTSerializer::functionBody(ParseNode* pn, TokenPos* pos, + MutableHandleValue dst) { + NodeVector elts(cx); + + // We aren't sure how many elements there are up front, so we'll check each + // append. + for (ParseNode* next = pn; next; next = next->pn_next) { + RootedValue child(cx); + if (!sourceElement(next, &child) || !elts.append(child)) { + return false; + } + } + + return builder.blockStatement(elts, pos, dst); +} + +static bool reflect_parse(JSContext* cx, uint32_t argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "Reflect.parse", 1)) { + return false; + } + + RootedString src(cx, ToString<CanGC>(cx, args[0])); + if (!src) { + return false; + } + + UniqueChars filename; + uint32_t lineno = 1; + bool loc = true; + ParseGoal target = ParseGoal::Script; + + RootedValue arg(cx, args.get(1)); + + if (!arg.isNullOrUndefined()) { + if (!arg.isObject()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, arg, + nullptr, "not an object"); + return false; + } + + RootedObject config(cx, &arg.toObject()); + + RootedValue prop(cx); + + /* config.loc */ + RootedId locId(cx, NameToId(cx->names().loc)); + RootedValue trueVal(cx, BooleanValue(true)); + if (!GetPropertyDefault(cx, config, locId, trueVal, &prop)) { + return false; + } + + loc = ToBoolean(prop); + + if (loc) { + /* config.source */ + RootedId sourceId(cx, NameToId(cx->names().source)); + RootedValue nullVal(cx, NullValue()); + if (!GetPropertyDefault(cx, config, sourceId, nullVal, &prop)) { + return false; + } + + if (!prop.isNullOrUndefined()) { + RootedString str(cx, ToString<CanGC>(cx, prop)); + if (!str) { + return false; + } + + filename = StringToNewUTF8CharsZ(cx, *str); + if (!filename) { + return false; + } + } + + /* config.line */ + RootedId lineId(cx, NameToId(cx->names().line)); + RootedValue oneValue(cx, Int32Value(1)); + if (!GetPropertyDefault(cx, config, lineId, oneValue, &prop) || + !ToUint32(cx, prop, &lineno)) { + return false; + } + } + + /* config.target */ + RootedId targetId(cx, NameToId(cx->names().target)); + RootedValue scriptVal(cx, StringValue(cx->names().script)); + if (!GetPropertyDefault(cx, config, targetId, scriptVal, &prop)) { + return false; + } + + if (!prop.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, prop, + nullptr, "not 'script' or 'module'"); + return false; + } + + RootedString stringProp(cx, prop.toString()); + bool isScript = false; + bool isModule = false; + if (!EqualStrings(cx, stringProp, cx->names().script, &isScript)) { + return false; + } + + if (!EqualStrings(cx, stringProp, cx->names().module, &isModule)) { + return false; + } + + if (isScript) { + target = ParseGoal::Script; + } else if (isModule) { + target = ParseGoal::Module; + } else { + JS_ReportErrorASCII(cx, + "Bad target value, expected 'script' or 'module'"); + return false; + } + } + + AutoReportFrontendContext fc(cx); + ASTSerializer serialize(cx, &fc, loc, filename.get(), lineno); + if (!serialize.init()) { + return false; + } + + JSLinearString* linear = src->ensureLinear(cx); + if (!linear) { + return false; + } + + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, linear)) { + return false; + } + + CompileOptions options(cx); + options.setFileAndLine(filename.get(), lineno); + options.setForceFullParse(); + options.allowHTMLComments = target == ParseGoal::Script; + mozilla::Range<const char16_t> chars = linearChars.twoByteRange(); + + Rooted<CompilationInput> input(cx, CompilationInput(options)); + if (target == ParseGoal::Script) { + if (!input.get().initForGlobal(&fc)) { + return false; + } + } else { + if (!input.get().initForModule(&fc)) { + return false; + } + } + + LifoAllocScope allocScope(&cx->tempLifoAlloc()); + frontend::NoScopeBindingCache scopeCache; + frontend::CompilationState compilationState(&fc, allocScope, input.get()); + if (!compilationState.init(&fc, &scopeCache)) { + return false; + } + + Parser<FullParseHandler, char16_t> parser( + &fc, options, chars.begin().get(), chars.length(), + /* foldConstants = */ false, compilationState, + /* syntaxParser = */ nullptr); + if (!parser.checkOptions()) { + return false; + } + + serialize.setParser(&parser); + + ParseNode* pn; + if (target == ParseGoal::Script) { + pn = parser.parse(); + if (!pn) { + return false; + } + } else { + ModuleBuilder builder(&fc, &parser); + + uint32_t len = chars.length(); + SourceExtent extent = + SourceExtent::makeGlobalExtent(len, options.lineno, options.column); + ModuleSharedContext modulesc(&fc, options, builder, extent); + pn = parser.moduleBody(&modulesc); + if (!pn) { + return false; + } + + pn = pn->as<ModuleNode>().body(); + } + + RootedValue val(cx); + if (!serialize.program(&pn->as<ListNode>(), &val)) { + args.rval().setNull(); + return false; + } + + args.rval().set(val); + return true; +} + +JS_PUBLIC_API bool JS_InitReflectParse(JSContext* cx, HandleObject global) { + RootedValue reflectVal(cx); + if (!GetProperty(cx, global, global, cx->names().Reflect, &reflectVal)) { + return false; + } + if (!reflectVal.isObject()) { + JS_ReportErrorASCII( + cx, "JS_InitReflectParse must be called during global initialization"); + return false; + } + + RootedObject reflectObj(cx, &reflectVal.toObject()); + return JS_DefineFunction(cx, reflectObj, "parse", reflect_parse, 1, 0); +} diff --git a/js/src/builtin/RegExp.cpp b/js/src/builtin/RegExp.cpp new file mode 100644 index 0000000000..fce8a66815 --- /dev/null +++ b/js/src/builtin/RegExp.cpp @@ -0,0 +1,2369 @@ +/* -*- 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 "builtin/RegExp.h" + +#include "mozilla/Casting.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/TextUtils.h" + +#include "jsapi.h" + +#include "frontend/FrontendContext.h" // AutoReportFrontendContext +#include "frontend/TokenStream.h" +#include "irregexp/RegExpAPI.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_NEWREGEXP_FLAGGED +#include "js/PropertySpec.h" +#include "js/RegExpFlags.h" // JS::RegExpFlag, JS::RegExpFlags +#include "util/StringBuffer.h" +#include "util/Unicode.h" +#include "vm/Interpreter.h" +#include "vm/JSContext.h" +#include "vm/RegExpObject.h" +#include "vm/RegExpStatics.h" +#include "vm/SelfHosting.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/EnvironmentObject-inl.h" +#include "vm/GeckoProfiler-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/ObjectOperations-inl.h" +#include "vm/PlainObject-inl.h" + +using namespace js; + +using mozilla::AssertedCast; +using mozilla::CheckedInt; +using mozilla::IsAsciiDigit; + +using JS::CompileOptions; +using JS::RegExpFlag; +using JS::RegExpFlags; + +// Allocate an object for the |.groups| or |.indices.groups| property +// of a regexp match result. +static PlainObject* CreateGroupsObject(JSContext* cx, + Handle<PlainObject*> groupsTemplate) { + if (groupsTemplate->inDictionaryMode()) { + return NewPlainObjectWithProto(cx, nullptr); + } + + // The groups template object is stored in RegExpShared, which is shared + // across compartments and realms. So watch out for the case when the template + // object's realm is different from the current realm. + if (cx->realm() != groupsTemplate->realm()) { + return PlainObject::createWithTemplateFromDifferentRealm(cx, + groupsTemplate); + } + + return PlainObject::createWithTemplate(cx, groupsTemplate); +} + +/* + * Implements RegExpBuiltinExec: Steps 18-35 + * https://tc39.es/ecma262/#sec-regexpbuiltinexec + */ +bool js::CreateRegExpMatchResult(JSContext* cx, HandleRegExpShared re, + HandleString input, const MatchPairs& matches, + MutableHandleValue rval) { + MOZ_ASSERT(re); + MOZ_ASSERT(input); + + /* + * Create the (slow) result array for a match. + * + * Array contents: + * 0: matched string + * 1..pairCount-1: paren matches + * input: input string + * index: start index for the match + * groups: named capture groups for the match + * indices: capture indices for the match, if required + */ + + bool hasIndices = re->hasIndices(); + + // Get the templateObject that defines the shape and type of the output + // object. + RegExpRealm::ResultTemplateKind kind = + hasIndices ? RegExpRealm::ResultTemplateKind::WithIndices + : RegExpRealm::ResultTemplateKind::Normal; + ArrayObject* templateObject = + cx->realm()->regExps.getOrCreateMatchResultTemplateObject(cx, kind); + if (!templateObject) { + return false; + } + + // Steps 18-19 + size_t numPairs = matches.length(); + MOZ_ASSERT(numPairs > 0); + + // Steps 20-21: Allocate the match result object. + Rooted<ArrayObject*> arr(cx, NewDenseFullyAllocatedArrayWithTemplate( + cx, numPairs, templateObject)); + if (!arr) { + return false; + } + + // Steps 28-29 and 33 a-d: Initialize the elements of the match result. + // Store a Value for each match pair. + for (size_t i = 0; i < numPairs; i++) { + const MatchPair& pair = matches[i]; + + if (pair.isUndefined()) { + MOZ_ASSERT(i != 0); // Since we had a match, first pair must be present. + arr->setDenseInitializedLength(i + 1); + arr->initDenseElement(i, UndefinedValue()); + } else { + JSLinearString* str = + NewDependentString(cx, input, pair.start, pair.length()); + if (!str) { + return false; + } + arr->setDenseInitializedLength(i + 1); + arr->initDenseElement(i, StringValue(str)); + } + } + + // Step 34a (reordered): Allocate and initialize the indices object if needed. + // This is an inlined implementation of MakeIndicesArray: + // https://tc39.es/ecma262/#sec-makeindicesarray + Rooted<ArrayObject*> indices(cx); + Rooted<PlainObject*> indicesGroups(cx); + if (hasIndices) { + // MakeIndicesArray: step 8 + ArrayObject* indicesTemplate = + cx->realm()->regExps.getOrCreateMatchResultTemplateObject( + cx, RegExpRealm::ResultTemplateKind::Indices); + indices = + NewDenseFullyAllocatedArrayWithTemplate(cx, numPairs, indicesTemplate); + if (!indices) { + return false; + } + + // MakeIndicesArray: steps 10-12 + if (re->numNamedCaptures() > 0) { + Rooted<PlainObject*> groupsTemplate(cx, re->getGroupsTemplate()); + indicesGroups = CreateGroupsObject(cx, groupsTemplate); + if (!indicesGroups) { + return false; + } + indices->setSlot(RegExpRealm::IndicesGroupsSlot, + ObjectValue(*indicesGroups)); + } else { + indices->setSlot(RegExpRealm::IndicesGroupsSlot, UndefinedValue()); + } + + // MakeIndicesArray: step 13 a-d. (Step 13.e is implemented below.) + for (size_t i = 0; i < numPairs; i++) { + const MatchPair& pair = matches[i]; + + if (pair.isUndefined()) { + // Since we had a match, first pair must be present. + MOZ_ASSERT(i != 0); + indices->setDenseInitializedLength(i + 1); + indices->initDenseElement(i, UndefinedValue()); + } else { + Rooted<ArrayObject*> indexPair(cx, NewDenseFullyAllocatedArray(cx, 2)); + if (!indexPair) { + return false; + } + indexPair->setDenseInitializedLength(2); + indexPair->initDenseElement(0, Int32Value(pair.start)); + indexPair->initDenseElement(1, Int32Value(pair.limit)); + + indices->setDenseInitializedLength(i + 1); + indices->initDenseElement(i, ObjectValue(*indexPair)); + } + } + } + + // Steps 30-31 (reordered): Allocate the groups object (if needed). + Rooted<PlainObject*> groups(cx); + bool groupsInDictionaryMode = false; + if (re->numNamedCaptures() > 0) { + Rooted<PlainObject*> groupsTemplate(cx, re->getGroupsTemplate()); + groupsInDictionaryMode = groupsTemplate->inDictionaryMode(); + groups = CreateGroupsObject(cx, groupsTemplate); + if (!groups) { + return false; + } + } + + // Step 33 e-f: Initialize the properties of |groups| and |indices.groups|. + // The groups template object stores the names of the named captures + // in the the order in which they are defined. The named capture + // indices vector stores the corresponding capture indices. In + // dictionary mode, we have to define the properties explicitly. If + // we are not in dictionary mode, we simply fill in the slots with + // the correct values. + if (groupsInDictionaryMode) { + RootedIdVector keys(cx); + Rooted<PlainObject*> groupsTemplate(cx, re->getGroupsTemplate()); + if (!GetPropertyKeys(cx, groupsTemplate, 0, &keys)) { + return false; + } + MOZ_ASSERT(keys.length() == re->numNamedCaptures()); + RootedId key(cx); + RootedValue val(cx); + for (uint32_t i = 0; i < keys.length(); i++) { + key = keys[i]; + uint32_t idx = re->getNamedCaptureIndex(i); + val = arr->getDenseElement(idx); + if (!NativeDefineDataProperty(cx, groups, key, val, JSPROP_ENUMERATE)) { + return false; + } + // MakeIndicesArray: Step 13.e (reordered) + if (hasIndices) { + val = indices->getDenseElement(idx); + if (!NativeDefineDataProperty(cx, indicesGroups, key, val, + JSPROP_ENUMERATE)) { + return false; + } + } + } + } else { + for (uint32_t i = 0; i < re->numNamedCaptures(); i++) { + uint32_t idx = re->getNamedCaptureIndex(i); + groups->setSlot(i, arr->getDenseElement(idx)); + + // MakeIndicesArray: Step 13.e (reordered) + if (hasIndices) { + indicesGroups->setSlot(i, indices->getDenseElement(idx)); + } + } + } + + // Step 22 (reordered). + // Set the |index| property. + arr->setSlot(RegExpRealm::MatchResultObjectIndexSlot, + Int32Value(matches[0].start)); + + // Step 23 (reordered). + // Set the |input| property. + arr->setSlot(RegExpRealm::MatchResultObjectInputSlot, StringValue(input)); + + // Step 32 (reordered) + // Set the |groups| property. + arr->setSlot(RegExpRealm::MatchResultObjectGroupsSlot, + groups ? ObjectValue(*groups) : UndefinedValue()); + + // Step 34b + // Set the |indices| property. + if (re->hasIndices()) { + arr->setSlot(RegExpRealm::MatchResultObjectIndicesSlot, + ObjectValue(*indices)); + } + +#ifdef DEBUG + RootedValue test(cx); + RootedId id(cx, NameToId(cx->names().index)); + if (!NativeGetProperty(cx, arr, id, &test)) { + return false; + } + MOZ_ASSERT(test == arr->getSlot(RegExpRealm::MatchResultObjectIndexSlot)); + id = NameToId(cx->names().input); + if (!NativeGetProperty(cx, arr, id, &test)) { + return false; + } + MOZ_ASSERT(test == arr->getSlot(RegExpRealm::MatchResultObjectInputSlot)); +#endif + + // Step 35. + rval.setObject(*arr); + return true; +} + +static int32_t CreateRegExpSearchResult(const MatchPairs& matches) { + /* Fit the start and limit of match into a int32_t. */ + uint32_t position = matches[0].start; + uint32_t lastIndex = matches[0].limit; + MOZ_ASSERT(position < 0x8000); + MOZ_ASSERT(lastIndex < 0x8000); + return position | (lastIndex << 15); +} + +/* + * ES 2017 draft rev 6a13789aa9e7c6de4e96b7d3e24d9e6eba6584ad 21.2.5.2.2 + * steps 3, 9-14, except 12.a.i, 12.c.i.1. + */ +static RegExpRunStatus ExecuteRegExpImpl(JSContext* cx, RegExpStatics* res, + MutableHandleRegExpShared re, + Handle<JSLinearString*> input, + size_t searchIndex, + VectorMatchPairs* matches) { + RegExpRunStatus status = + RegExpShared::execute(cx, re, input, searchIndex, matches); + + /* Out of spec: Update RegExpStatics. */ + if (status == RegExpRunStatus_Success && res) { + if (!res->updateFromMatchPairs(cx, input, *matches)) { + return RegExpRunStatus_Error; + } + } + return status; +} + +/* Legacy ExecuteRegExp behavior is baked into the JSAPI. */ +bool js::ExecuteRegExpLegacy(JSContext* cx, RegExpStatics* res, + Handle<RegExpObject*> reobj, + Handle<JSLinearString*> input, size_t* lastIndex, + bool test, MutableHandleValue rval) { + cx->check(reobj, input); + + RootedRegExpShared shared(cx, RegExpObject::getShared(cx, reobj)); + if (!shared) { + return false; + } + + VectorMatchPairs matches; + + RegExpRunStatus status = + ExecuteRegExpImpl(cx, res, &shared, input, *lastIndex, &matches); + if (status == RegExpRunStatus_Error) { + return false; + } + + if (status == RegExpRunStatus_Success_NotFound) { + /* ExecuteRegExp() previously returned an array or null. */ + rval.setNull(); + return true; + } + + *lastIndex = matches[0].limit; + + if (test) { + /* Forbid an array, as an optimization. */ + rval.setBoolean(true); + return true; + } + + return CreateRegExpMatchResult(cx, shared, input, matches, rval); +} + +static bool CheckPatternSyntaxSlow(JSContext* cx, Handle<JSAtom*> pattern, + RegExpFlags flags) { + LifoAllocScope allocScope(&cx->tempLifoAlloc()); + AutoReportFrontendContext fc(cx); + CompileOptions options(cx); + frontend::DummyTokenStream dummyTokenStream(&fc, options); + return irregexp::CheckPatternSyntax(cx, cx->stackLimitForCurrentPrincipal(), + dummyTokenStream, pattern, flags); +} + +static RegExpShared* CheckPatternSyntax(JSContext* cx, Handle<JSAtom*> pattern, + RegExpFlags flags) { + // If we already have a RegExpShared for this pattern/flags, we can + // avoid the much slower CheckPatternSyntaxSlow call. + + RootedRegExpShared shared(cx, cx->zone()->regExps().maybeGet(pattern, flags)); + if (shared) { +#ifdef DEBUG + // Assert the pattern is valid. + if (!CheckPatternSyntaxSlow(cx, pattern, flags)) { + MOZ_ASSERT(cx->isThrowingOutOfMemory() || cx->isThrowingOverRecursed()); + return nullptr; + } +#endif + return shared; + } + + if (!CheckPatternSyntaxSlow(cx, pattern, flags)) { + return nullptr; + } + + // Allocate and return a new RegExpShared so we will hit the fast path + // next time. + return cx->zone()->regExps().get(cx, pattern, flags); +} + +/* + * ES 2016 draft Mar 25, 2016 21.2.3.2.2. + * + * Steps 14-15 set |obj|'s "lastIndex" property to zero. Some of + * RegExpInitialize's callers have a fresh RegExp not yet exposed to script: + * in these cases zeroing "lastIndex" is infallible. But others have a RegExp + * whose "lastIndex" property might have been made non-writable: here, zeroing + * "lastIndex" can fail. We efficiently solve this problem by completely + * removing "lastIndex" zeroing from the provided function. + * + * CALLERS MUST HANDLE "lastIndex" ZEROING THEMSELVES! + * + * Because this function only ever returns a user-provided |obj| in the spec, + * we omit it and just return the usual success/failure. + */ +static bool RegExpInitializeIgnoringLastIndex(JSContext* cx, + Handle<RegExpObject*> obj, + HandleValue patternValue, + HandleValue flagsValue) { + Rooted<JSAtom*> pattern(cx); + if (patternValue.isUndefined()) { + /* Step 1. */ + pattern = cx->names().empty; + } else { + /* Step 2. */ + pattern = ToAtom<CanGC>(cx, patternValue); + if (!pattern) { + return false; + } + } + + /* Step 3. */ + RegExpFlags flags = RegExpFlag::NoFlags; + if (!flagsValue.isUndefined()) { + /* Step 4. */ + RootedString flagStr(cx, ToString<CanGC>(cx, flagsValue)); + if (!flagStr) { + return false; + } + + /* Step 5. */ + if (!ParseRegExpFlags(cx, flagStr, &flags)) { + return false; + } + } + + /* Steps 7-8. */ + RegExpShared* shared = CheckPatternSyntax(cx, pattern, flags); + if (!shared) { + return false; + } + + /* Steps 9-12. */ + obj->initIgnoringLastIndex(pattern, flags); + + obj->setShared(shared); + + return true; +} + +/* ES 2016 draft Mar 25, 2016 21.2.3.2.3. */ +bool js::RegExpCreate(JSContext* cx, HandleValue patternValue, + HandleValue flagsValue, MutableHandleValue rval) { + /* Step 1. */ + Rooted<RegExpObject*> regexp(cx, RegExpAlloc(cx, GenericObject)); + if (!regexp) { + return false; + } + + /* Step 2. */ + if (!RegExpInitializeIgnoringLastIndex(cx, regexp, patternValue, + flagsValue)) { + return false; + } + regexp->zeroLastIndex(cx); + + rval.setObject(*regexp); + return true; +} + +MOZ_ALWAYS_INLINE bool IsRegExpObject(HandleValue v) { + return v.isObject() && v.toObject().is<RegExpObject>(); +} + +/* ES6 draft rc3 7.2.8. */ +bool js::IsRegExp(JSContext* cx, HandleValue value, bool* result) { + /* Step 1. */ + if (!value.isObject()) { + *result = false; + return true; + } + RootedObject obj(cx, &value.toObject()); + + /* Steps 2-3. */ + RootedValue isRegExp(cx); + RootedId matchId(cx, PropertyKey::Symbol(cx->wellKnownSymbols().match)); + if (!GetProperty(cx, obj, obj, matchId, &isRegExp)) { + return false; + } + + /* Step 4. */ + if (!isRegExp.isUndefined()) { + *result = ToBoolean(isRegExp); + return true; + } + + /* Steps 5-6. */ + ESClass cls; + if (!GetClassOfValue(cx, value, &cls)) { + return false; + } + + *result = cls == ESClass::RegExp; + return true; +} + +// The "lastIndex" property is non-configurable, but it can be made +// non-writable. If CalledFromJit is true, we have emitted guards to ensure it's +// writable. +template <bool CalledFromJit = false> +static bool SetLastIndex(JSContext* cx, Handle<RegExpObject*> regexp, + int32_t lastIndex) { + MOZ_ASSERT(lastIndex >= 0); + + if (CalledFromJit || MOZ_LIKELY(RegExpObject::isInitialShape(regexp)) || + regexp->lookupPure(cx->names().lastIndex)->writable()) { + regexp->setLastIndex(cx, lastIndex); + return true; + } + + Rooted<Value> val(cx, Int32Value(lastIndex)); + return SetProperty(cx, regexp, cx->names().lastIndex, val); +} + +/* ES6 B.2.5.1. */ +MOZ_ALWAYS_INLINE bool regexp_compile_impl(JSContext* cx, + const CallArgs& args) { + MOZ_ASSERT(IsRegExpObject(args.thisv())); + + Rooted<RegExpObject*> regexp(cx, &args.thisv().toObject().as<RegExpObject>()); + + // Step 3. + RootedValue patternValue(cx, args.get(0)); + ESClass cls; + if (!GetClassOfValue(cx, patternValue, &cls)) { + return false; + } + if (cls == ESClass::RegExp) { + // Step 3a. + if (args.hasDefined(1)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NEWREGEXP_FLAGGED); + return false; + } + + // Beware! |patternObj| might be a proxy into another compartment, so + // don't assume |patternObj.is<RegExpObject>()|. For the same reason, + // don't reuse the RegExpShared below. + RootedObject patternObj(cx, &patternValue.toObject()); + + Rooted<JSAtom*> sourceAtom(cx); + RegExpFlags flags = RegExpFlag::NoFlags; + { + // Step 3b. + RegExpShared* shared = RegExpToShared(cx, patternObj); + if (!shared) { + return false; + } + + sourceAtom = shared->getSource(); + flags = shared->getFlags(); + } + + // Step 5, minus lastIndex zeroing. + regexp->initIgnoringLastIndex(sourceAtom, flags); + } else { + // Step 4. + RootedValue P(cx, patternValue); + RootedValue F(cx, args.get(1)); + + // Step 5, minus lastIndex zeroing. + if (!RegExpInitializeIgnoringLastIndex(cx, regexp, P, F)) { + return false; + } + } + + // The final niggling bit of step 5. + // + // |regexp| is user-exposed, so its "lastIndex" property might be + // non-writable. + if (!SetLastIndex(cx, regexp, 0)) { + return false; + } + + args.rval().setObject(*regexp); + return true; +} + +static bool regexp_compile(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + /* Steps 1-2. */ + return CallNonGenericMethod<IsRegExpObject, regexp_compile_impl>(cx, args); +} + +/* + * ES 2017 draft rev 6a13789aa9e7c6de4e96b7d3e24d9e6eba6584ad 21.2.3.1. + */ +bool js::regexp_construct(JSContext* cx, unsigned argc, Value* vp) { + AutoJSConstructorProfilerEntry pseudoFrame(cx, "RegExp"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1. + bool patternIsRegExp; + if (!IsRegExp(cx, args.get(0), &patternIsRegExp)) { + return false; + } + + // We can delay step 3 and step 4a until later, during + // GetPrototypeFromBuiltinConstructor calls. Accessing the new.target + // and the callee from the stack is unobservable. + if (!args.isConstructing()) { + // Step 3.b. + if (patternIsRegExp && !args.hasDefined(1)) { + RootedObject patternObj(cx, &args[0].toObject()); + + // Step 3.b.i. + RootedValue patternConstructor(cx); + if (!GetProperty(cx, patternObj, patternObj, cx->names().constructor, + &patternConstructor)) { + return false; + } + + // Step 3.b.ii. + if (patternConstructor.isObject() && + patternConstructor.toObject() == args.callee()) { + args.rval().set(args[0]); + return true; + } + } + } + + RootedValue patternValue(cx, args.get(0)); + + // Step 4. + ESClass cls; + if (!GetClassOfValue(cx, patternValue, &cls)) { + return false; + } + if (cls == ESClass::RegExp) { + // Beware! |patternObj| might be a proxy into another compartment, so + // don't assume |patternObj.is<RegExpObject>()|. + RootedObject patternObj(cx, &patternValue.toObject()); + + Rooted<JSAtom*> sourceAtom(cx); + RegExpFlags flags; + RootedRegExpShared shared(cx); + { + // Step 4.a. + shared = RegExpToShared(cx, patternObj); + if (!shared) { + return false; + } + sourceAtom = shared->getSource(); + + // Step 4.b. + // Get original flags in all cases, to compare with passed flags. + flags = shared->getFlags(); + + // If the RegExpShared is in another Zone, don't reuse it. + if (cx->zone() != shared->zone()) { + shared = nullptr; + } + } + + // Step 7. + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_RegExp, &proto)) { + return false; + } + + Rooted<RegExpObject*> regexp(cx, RegExpAlloc(cx, GenericObject, proto)); + if (!regexp) { + return false; + } + + // Step 8. + if (args.hasDefined(1)) { + // Step 4.c / 21.2.3.2.2 RegExpInitialize step 4. + RegExpFlags flagsArg = RegExpFlag::NoFlags; + RootedString flagStr(cx, ToString<CanGC>(cx, args[1])); + if (!flagStr) { + return false; + } + if (!ParseRegExpFlags(cx, flagStr, &flagsArg)) { + return false; + } + + // Don't reuse the RegExpShared if we have different flags. + if (flags != flagsArg) { + shared = nullptr; + } + + if (!flags.unicode() && flagsArg.unicode()) { + // Have to check syntax again when adding 'u' flag. + + // ES 2017 draft rev 9b49a888e9dfe2667008a01b2754c3662059ae56 + // 21.2.3.2.2 step 7. + shared = CheckPatternSyntax(cx, sourceAtom, flagsArg); + if (!shared) { + return false; + } + } + flags = flagsArg; + } + + regexp->initAndZeroLastIndex(sourceAtom, flags, cx); + + if (shared) { + regexp->setShared(shared); + } + + args.rval().setObject(*regexp); + return true; + } + + RootedValue P(cx); + RootedValue F(cx); + + // Step 5. + if (patternIsRegExp) { + RootedObject patternObj(cx, &patternValue.toObject()); + + // Step 5.a. + if (!GetProperty(cx, patternObj, patternObj, cx->names().source, &P)) { + return false; + } + + // Step 5.b. + F = args.get(1); + if (F.isUndefined()) { + if (!GetProperty(cx, patternObj, patternObj, cx->names().flags, &F)) { + return false; + } + } + } else { + // Steps 6.a-b. + P = patternValue; + F = args.get(1); + } + + // Step 7. + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_RegExp, &proto)) { + return false; + } + + Rooted<RegExpObject*> regexp(cx, RegExpAlloc(cx, GenericObject, proto)); + if (!regexp) { + return false; + } + + // Step 8. + if (!RegExpInitializeIgnoringLastIndex(cx, regexp, P, F)) { + return false; + } + regexp->zeroLastIndex(cx); + + args.rval().setObject(*regexp); + return true; +} + +/* + * ES 2017 draft rev 6a13789aa9e7c6de4e96b7d3e24d9e6eba6584ad 21.2.3.1 + * steps 4, 7-8. + */ +bool js::regexp_construct_raw_flags(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + + // Step 4.a. + Rooted<JSAtom*> sourceAtom(cx, AtomizeString(cx, args[0].toString())); + if (!sourceAtom) { + return false; + } + + // Step 4.c. + RegExpFlags flags = AssertedCast<uint8_t>(int32_t(args[1].toNumber())); + + // Step 7. + RegExpObject* regexp = RegExpAlloc(cx, GenericObject); + if (!regexp) { + return false; + } + + // Step 8. + regexp->initAndZeroLastIndex(sourceAtom, flags, cx); + args.rval().setObject(*regexp); + return true; +} + +// This is a specialized implementation of "UnwrapAndTypeCheckThis" for RegExp +// getters that need to return a special value for same-realm +// %RegExp.prototype%. +template <typename Fn> +static bool RegExpGetter(JSContext* cx, CallArgs& args, const char* methodName, + Fn&& fn, + HandleValue fallbackValue = UndefinedHandleValue) { + JSObject* obj = nullptr; + if (args.thisv().isObject()) { + obj = &args.thisv().toObject(); + if (IsWrapper(obj)) { + obj = CheckedUnwrapStatic(obj); + if (!obj) { + ReportAccessDenied(cx); + return false; + } + } + } + + if (obj) { + // Step 4ff + if (obj->is<RegExpObject>()) { + return fn(&obj->as<RegExpObject>()); + } + + // Step 3.a. "If SameValue(R, %RegExp.prototype%) is true, return + // undefined." + // Or `return "(?:)"` for get RegExp.prototype.source. + if (obj == cx->global()->maybeGetRegExpPrototype()) { + args.rval().set(fallbackValue); + return true; + } + + // fall-through + } + + // Step 2. and Step 3.b. + JS_ReportErrorNumberLatin1(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_REGEXP_GETTER, methodName, + InformalValueTypeName(args.thisv())); + return false; +} + +bool js::regexp_hasIndices(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return RegExpGetter(cx, args, "hasIndices", [args](RegExpObject* unwrapped) { + args.rval().setBoolean(unwrapped->hasIndices()); + return true; + }); +} + +// ES2021 draft rev 0b3a808af87a9123890767152a26599cc8fde161 +// 21.2.5.5 get RegExp.prototype.global +bool js::regexp_global(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return RegExpGetter(cx, args, "global", [args](RegExpObject* unwrapped) { + args.rval().setBoolean(unwrapped->global()); + return true; + }); +} + +// ES2021 draft rev 0b3a808af87a9123890767152a26599cc8fde161 +// 21.2.5.6 get RegExp.prototype.ignoreCase +bool js::regexp_ignoreCase(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return RegExpGetter(cx, args, "ignoreCase", [args](RegExpObject* unwrapped) { + args.rval().setBoolean(unwrapped->ignoreCase()); + return true; + }); +} + +// ES2021 draft rev 0b3a808af87a9123890767152a26599cc8fde161 +// 21.2.5.9 get RegExp.prototype.multiline +bool js::regexp_multiline(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return RegExpGetter(cx, args, "multiline", [args](RegExpObject* unwrapped) { + args.rval().setBoolean(unwrapped->multiline()); + return true; + }); +} + +// ES2021 draft rev 0b3a808af87a9123890767152a26599cc8fde161 +// 21.2.5.12 get RegExp.prototype.source +static bool regexp_source(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + // Step 3.a. Return "(?:)" for %RegExp.prototype%. + RootedValue fallback(cx, StringValue(cx->names().emptyRegExp)); + return RegExpGetter( + cx, args, "source", + [cx, args](RegExpObject* unwrapped) { + Rooted<JSAtom*> src(cx, unwrapped->getSource()); + MOZ_ASSERT(src); + // Mark potentially cross-zone JSAtom. + if (cx->zone() != unwrapped->zone()) { + cx->markAtom(src); + } + + // Step 7. + JSString* escaped = EscapeRegExpPattern(cx, src); + if (!escaped) { + return false; + } + + args.rval().setString(escaped); + return true; + }, + fallback); +} + +// ES2021 draft rev 0b3a808af87a9123890767152a26599cc8fde161 +// 21.2.5.3 get RegExp.prototype.dotAll +bool js::regexp_dotAll(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return RegExpGetter(cx, args, "dotAll", [args](RegExpObject* unwrapped) { + args.rval().setBoolean(unwrapped->dotAll()); + return true; + }); +} + +// ES2021 draft rev 0b3a808af87a9123890767152a26599cc8fde161 +// 21.2.5.14 get RegExp.prototype.sticky +bool js::regexp_sticky(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return RegExpGetter(cx, args, "sticky", [args](RegExpObject* unwrapped) { + args.rval().setBoolean(unwrapped->sticky()); + return true; + }); +} + +// ES2021 draft rev 0b3a808af87a9123890767152a26599cc8fde161 +// 21.2.5.17 get RegExp.prototype.unicode +bool js::regexp_unicode(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return RegExpGetter(cx, args, "unicode", [args](RegExpObject* unwrapped) { + args.rval().setBoolean(unwrapped->unicode()); + return true; + }); +} + +const JSPropertySpec js::regexp_properties[] = { + JS_SELF_HOSTED_GET("flags", "$RegExpFlagsGetter", 0), + JS_PSG("hasIndices", regexp_hasIndices, 0), + JS_PSG("global", regexp_global, 0), + JS_PSG("ignoreCase", regexp_ignoreCase, 0), + JS_PSG("multiline", regexp_multiline, 0), + JS_PSG("dotAll", regexp_dotAll, 0), + JS_PSG("source", regexp_source, 0), + JS_PSG("sticky", regexp_sticky, 0), + JS_PSG("unicode", regexp_unicode, 0), + JS_PS_END}; + +const JSFunctionSpec js::regexp_methods[] = { + JS_SELF_HOSTED_FN(js_toSource_str, "$RegExpToString", 0, 0), + JS_SELF_HOSTED_FN(js_toString_str, "$RegExpToString", 0, 0), + JS_FN("compile", regexp_compile, 2, 0), + JS_SELF_HOSTED_FN("exec", "RegExp_prototype_Exec", 1, 0), + JS_SELF_HOSTED_FN("test", "RegExpTest", 1, 0), + JS_SELF_HOSTED_SYM_FN(match, "RegExpMatch", 1, 0), + JS_SELF_HOSTED_SYM_FN(matchAll, "RegExpMatchAll", 1, 0), + JS_SELF_HOSTED_SYM_FN(replace, "RegExpReplace", 2, 0), + JS_SELF_HOSTED_SYM_FN(search, "RegExpSearch", 1, 0), + JS_SELF_HOSTED_SYM_FN(split, "RegExpSplit", 2, 0), + JS_FS_END}; + +#define STATIC_PAREN_GETTER_CODE(parenNum) \ + if (!res->createParen(cx, parenNum, args.rval())) return false; \ + if (args.rval().isUndefined()) \ + args.rval().setString(cx->runtime()->emptyString); \ + return true + +/* + * RegExp static properties. + * + * RegExp class static properties and their Perl counterparts: + * + * RegExp.input $_ + * RegExp.lastMatch $& + * RegExp.lastParen $+ + * RegExp.leftContext $` + * RegExp.rightContext $' + */ + +#define DEFINE_STATIC_GETTER(name, code) \ + static bool name(JSContext* cx, unsigned argc, Value* vp) { \ + CallArgs args = CallArgsFromVp(argc, vp); \ + RegExpStatics* res = GlobalObject::getRegExpStatics(cx, cx->global()); \ + if (!res) return false; \ + code; \ + } + +DEFINE_STATIC_GETTER(static_input_getter, + return res->createPendingInput(cx, args.rval())) +DEFINE_STATIC_GETTER(static_lastMatch_getter, + return res->createLastMatch(cx, args.rval())) +DEFINE_STATIC_GETTER(static_lastParen_getter, + return res->createLastParen(cx, args.rval())) +DEFINE_STATIC_GETTER(static_leftContext_getter, + return res->createLeftContext(cx, args.rval())) +DEFINE_STATIC_GETTER(static_rightContext_getter, + return res->createRightContext(cx, args.rval())) + +DEFINE_STATIC_GETTER(static_paren1_getter, STATIC_PAREN_GETTER_CODE(1)) +DEFINE_STATIC_GETTER(static_paren2_getter, STATIC_PAREN_GETTER_CODE(2)) +DEFINE_STATIC_GETTER(static_paren3_getter, STATIC_PAREN_GETTER_CODE(3)) +DEFINE_STATIC_GETTER(static_paren4_getter, STATIC_PAREN_GETTER_CODE(4)) +DEFINE_STATIC_GETTER(static_paren5_getter, STATIC_PAREN_GETTER_CODE(5)) +DEFINE_STATIC_GETTER(static_paren6_getter, STATIC_PAREN_GETTER_CODE(6)) +DEFINE_STATIC_GETTER(static_paren7_getter, STATIC_PAREN_GETTER_CODE(7)) +DEFINE_STATIC_GETTER(static_paren8_getter, STATIC_PAREN_GETTER_CODE(8)) +DEFINE_STATIC_GETTER(static_paren9_getter, STATIC_PAREN_GETTER_CODE(9)) + +#define DEFINE_STATIC_SETTER(name, code) \ + static bool name(JSContext* cx, unsigned argc, Value* vp) { \ + RegExpStatics* res = GlobalObject::getRegExpStatics(cx, cx->global()); \ + if (!res) return false; \ + code; \ + return true; \ + } + +static bool static_input_setter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RegExpStatics* res = GlobalObject::getRegExpStatics(cx, cx->global()); + if (!res) { + return false; + } + + RootedString str(cx, ToString<CanGC>(cx, args.get(0))); + if (!str) { + return false; + } + + res->setPendingInput(str); + args.rval().setString(str); + return true; +} + +const JSPropertySpec js::regexp_static_props[] = { + JS_PSGS("input", static_input_getter, static_input_setter, + JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSG("lastMatch", static_lastMatch_getter, + JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSG("lastParen", static_lastParen_getter, + JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSG("leftContext", static_leftContext_getter, + JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSG("rightContext", static_rightContext_getter, + JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSG("$1", static_paren1_getter, JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSG("$2", static_paren2_getter, JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSG("$3", static_paren3_getter, JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSG("$4", static_paren4_getter, JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSG("$5", static_paren5_getter, JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSG("$6", static_paren6_getter, JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSG("$7", static_paren7_getter, JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSG("$8", static_paren8_getter, JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSG("$9", static_paren9_getter, JSPROP_PERMANENT | JSPROP_ENUMERATE), + JS_PSGS("$_", static_input_getter, static_input_setter, JSPROP_PERMANENT), + JS_PSG("$&", static_lastMatch_getter, JSPROP_PERMANENT), + JS_PSG("$+", static_lastParen_getter, JSPROP_PERMANENT), + JS_PSG("$`", static_leftContext_getter, JSPROP_PERMANENT), + JS_PSG("$'", static_rightContext_getter, JSPROP_PERMANENT), + JS_SELF_HOSTED_SYM_GET(species, "$RegExpSpecies", 0), + JS_PS_END}; + +template <typename CharT> +static bool IsTrailSurrogateWithLeadSurrogateImpl(Handle<JSLinearString*> input, + size_t index) { + JS::AutoCheckCannotGC nogc; + MOZ_ASSERT(index > 0 && index < input->length()); + const CharT* inputChars = input->chars<CharT>(nogc); + + return unicode::IsTrailSurrogate(inputChars[index]) && + unicode::IsLeadSurrogate(inputChars[index - 1]); +} + +static bool IsTrailSurrogateWithLeadSurrogate(Handle<JSLinearString*> input, + int32_t index) { + if (index <= 0 || size_t(index) >= input->length()) { + return false; + } + + return input->hasLatin1Chars() + ? IsTrailSurrogateWithLeadSurrogateImpl<Latin1Char>(input, index) + : IsTrailSurrogateWithLeadSurrogateImpl<char16_t>(input, index); +} + +/* + * ES 2017 draft rev 6a13789aa9e7c6de4e96b7d3e24d9e6eba6584ad 21.2.5.2.2 + * steps 3, 9-14, except 12.a.i, 12.c.i.1. + */ +static RegExpRunStatus ExecuteRegExp(JSContext* cx, HandleObject regexp, + HandleString string, int32_t lastIndex, + VectorMatchPairs* matches) { + /* + * WARNING: Despite the presence of spec step comment numbers, this + * algorithm isn't consistent with any ES6 version, draft or + * otherwise. YOU HAVE BEEN WARNED. + */ + + /* Steps 1-2 performed by the caller. */ + Handle<RegExpObject*> reobj = regexp.as<RegExpObject>(); + + RootedRegExpShared re(cx, RegExpObject::getShared(cx, reobj)); + if (!re) { + return RegExpRunStatus_Error; + } + + RegExpStatics* res = GlobalObject::getRegExpStatics(cx, cx->global()); + if (!res) { + return RegExpRunStatus_Error; + } + + Rooted<JSLinearString*> input(cx, string->ensureLinear(cx)); + if (!input) { + return RegExpRunStatus_Error; + } + + /* Handled by caller */ + MOZ_ASSERT(lastIndex >= 0 && size_t(lastIndex) <= input->length()); + + /* Steps 4-8 performed by the caller. */ + + /* Step 10. */ + if (reobj->unicode()) { + /* + * ES 2017 draft rev 6a13789aa9e7c6de4e96b7d3e24d9e6eba6584ad + * 21.2.2.2 step 2. + * Let listIndex be the index into Input of the character that was + * obtained from element index of str. + * + * In the spec, pattern match is performed with decoded Unicode code + * points, but our implementation performs it with UTF-16 encoded + * string. In step 2, we should decrement lastIndex (index) if it + * points the trail surrogate that has corresponding lead surrogate. + * + * var r = /\uD83D\uDC38/ug; + * r.lastIndex = 1; + * var str = "\uD83D\uDC38"; + * var result = r.exec(str); // pattern match starts from index 0 + * print(result.index); // prints 0 + * + * Note: this doesn't match the current spec text and result in + * different values for `result.index` under certain conditions. + * However, the spec will change to match our implementation's + * behavior. See https://github.com/tc39/ecma262/issues/128. + */ + if (IsTrailSurrogateWithLeadSurrogate(input, lastIndex)) { + lastIndex--; + } + } + + /* Steps 3, 11-14, except 12.a.i, 12.c.i.1. */ + RegExpRunStatus status = + ExecuteRegExpImpl(cx, res, &re, input, lastIndex, matches); + if (status == RegExpRunStatus_Error) { + return RegExpRunStatus_Error; + } + + /* Steps 12.a.i, 12.c.i.i, 15 are done by Self-hosted function. */ + + return status; +} + +/* + * ES 2017 draft rev 6a13789aa9e7c6de4e96b7d3e24d9e6eba6584ad 21.2.5.2.2 + * steps 3, 9-25, except 12.a.i, 12.c.i.1, 15. + */ +static bool RegExpMatcherImpl(JSContext* cx, HandleObject regexp, + HandleString string, int32_t lastIndex, + MutableHandleValue rval) { + /* Execute regular expression and gather matches. */ + VectorMatchPairs matches; + + /* Steps 3, 9-14, except 12.a.i, 12.c.i.1. */ + RegExpRunStatus status = + ExecuteRegExp(cx, regexp, string, lastIndex, &matches); + if (status == RegExpRunStatus_Error) { + return false; + } + + /* Steps 12.a, 12.c. */ + if (status == RegExpRunStatus_Success_NotFound) { + rval.setNull(); + return true; + } + + /* Steps 16-25 */ + RootedRegExpShared shared(cx, regexp->as<RegExpObject>().getShared()); + return CreateRegExpMatchResult(cx, shared, string, matches, rval); +} + +/* + * ES 2017 draft rev 6a13789aa9e7c6de4e96b7d3e24d9e6eba6584ad 21.2.5.2.2 + * steps 3, 9-25, except 12.a.i, 12.c.i.1, 15. + */ +bool js::RegExpMatcher(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(IsRegExpObject(args[0])); + MOZ_ASSERT(args[1].isString()); + MOZ_ASSERT(args[2].isNumber()); + + RootedObject regexp(cx, &args[0].toObject()); + RootedString string(cx, args[1].toString()); + + int32_t lastIndex; + MOZ_ALWAYS_TRUE(ToInt32(cx, args[2], &lastIndex)); + + /* Steps 3, 9-25, except 12.a.i, 12.c.i.1, 15. */ + return RegExpMatcherImpl(cx, regexp, string, lastIndex, args.rval()); +} + +/* + * Separate interface for use by the JITs. + * This code cannot re-enter JIT code. + */ +bool js::RegExpMatcherRaw(JSContext* cx, HandleObject regexp, + HandleString input, int32_t lastIndex, + MatchPairs* maybeMatches, MutableHandleValue output) { + MOZ_ASSERT(lastIndex >= 0 && size_t(lastIndex) <= input->length()); + + // RegExp execution was successful only if the pairs have actually been + // filled in. Note that IC code always passes a nullptr maybeMatches. + if (maybeMatches && maybeMatches->pairsRaw()[0] > MatchPair::NoMatch) { + RootedRegExpShared shared(cx, regexp->as<RegExpObject>().getShared()); + return CreateRegExpMatchResult(cx, shared, input, *maybeMatches, output); + } + return RegExpMatcherImpl(cx, regexp, input, lastIndex, output); +} + +/* + * ES 2017 draft rev 6a13789aa9e7c6de4e96b7d3e24d9e6eba6584ad 21.2.5.2.2 + * steps 3, 9-25, except 12.a.i, 12.c.i.1, 15. + * This code is inlined in CodeGenerator.cpp generateRegExpSearcherStub, + * changes to this code need to get reflected in there too. + */ +static bool RegExpSearcherImpl(JSContext* cx, HandleObject regexp, + HandleString string, int32_t lastIndex, + int32_t* result) { + /* Execute regular expression and gather matches. */ + VectorMatchPairs matches; + + /* Steps 3, 9-14, except 12.a.i, 12.c.i.1. */ + RegExpRunStatus status = + ExecuteRegExp(cx, regexp, string, lastIndex, &matches); + if (status == RegExpRunStatus_Error) { + return false; + } + + /* Steps 12.a, 12.c. */ + if (status == RegExpRunStatus_Success_NotFound) { + *result = -1; + return true; + } + + /* Steps 16-25 */ + *result = CreateRegExpSearchResult(matches); + return true; +} + +/* + * ES 2017 draft rev 6a13789aa9e7c6de4e96b7d3e24d9e6eba6584ad 21.2.5.2.2 + * steps 3, 9-25, except 12.a.i, 12.c.i.1, 15. + */ +bool js::RegExpSearcher(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(IsRegExpObject(args[0])); + MOZ_ASSERT(args[1].isString()); + MOZ_ASSERT(args[2].isNumber()); + + RootedObject regexp(cx, &args[0].toObject()); + RootedString string(cx, args[1].toString()); + + int32_t lastIndex; + MOZ_ALWAYS_TRUE(ToInt32(cx, args[2], &lastIndex)); + + /* Steps 3, 9-25, except 12.a.i, 12.c.i.1, 15. */ + int32_t result = 0; + if (!RegExpSearcherImpl(cx, regexp, string, lastIndex, &result)) { + return false; + } + + args.rval().setInt32(result); + return true; +} + +/* + * Separate interface for use by the JITs. + * This code cannot re-enter JIT code. + */ +bool js::RegExpSearcherRaw(JSContext* cx, HandleObject regexp, + HandleString input, int32_t lastIndex, + MatchPairs* maybeMatches, int32_t* result) { + MOZ_ASSERT(lastIndex >= 0); + + // RegExp execution was successful only if the pairs have actually been + // filled in. Note that IC code always passes a nullptr maybeMatches. + if (maybeMatches && maybeMatches->pairsRaw()[0] > MatchPair::NoMatch) { + *result = CreateRegExpSearchResult(*maybeMatches); + return true; + } + return RegExpSearcherImpl(cx, regexp, input, lastIndex, result); +} + +template <bool CalledFromJit> +static bool RegExpBuiltinExecMatchRaw(JSContext* cx, + Handle<RegExpObject*> regexp, + HandleString input, int32_t lastIndex, + MatchPairs* maybeMatches, + MutableHandleValue output) { + MOZ_ASSERT(lastIndex >= 0); + MOZ_ASSERT(size_t(lastIndex) <= input->length()); + MOZ_ASSERT_IF(!CalledFromJit, !maybeMatches); + + // RegExp execution was successful only if the pairs have actually been + // filled in. Note that IC code always passes a nullptr maybeMatches. + int32_t lastIndexNew = 0; + if (CalledFromJit && maybeMatches && + maybeMatches->pairsRaw()[0] > MatchPair::NoMatch) { + RootedRegExpShared shared(cx, regexp->as<RegExpObject>().getShared()); + if (!CreateRegExpMatchResult(cx, shared, input, *maybeMatches, output)) { + return false; + } + lastIndexNew = (*maybeMatches)[0].limit; + } else { + VectorMatchPairs matches; + RegExpRunStatus status = + ExecuteRegExp(cx, regexp, input, lastIndex, &matches); + if (status == RegExpRunStatus_Error) { + return false; + } + if (status == RegExpRunStatus_Success_NotFound) { + output.setNull(); + lastIndexNew = 0; + } else { + RootedRegExpShared shared(cx, regexp->as<RegExpObject>().getShared()); + if (!CreateRegExpMatchResult(cx, shared, input, matches, output)) { + return false; + } + lastIndexNew = matches[0].limit; + } + } + + RegExpFlags flags = regexp->getFlags(); + if (!flags.global() && !flags.sticky()) { + return true; + } + + return SetLastIndex<CalledFromJit>(cx, regexp, lastIndexNew); +} + +bool js::RegExpBuiltinExecMatchFromJit(JSContext* cx, + Handle<RegExpObject*> regexp, + HandleString input, + MatchPairs* maybeMatches, + MutableHandleValue output) { + int32_t lastIndex = 0; + if (regexp->isGlobalOrSticky()) { + lastIndex = regexp->getLastIndex().toInt32(); + MOZ_ASSERT(lastIndex >= 0); + if (size_t(lastIndex) > input->length()) { + output.setNull(); + return SetLastIndex<true>(cx, regexp, 0); + } + } + return RegExpBuiltinExecMatchRaw<true>(cx, regexp, input, lastIndex, + maybeMatches, output); +} + +template <bool CalledFromJit> +static bool RegExpBuiltinExecTestRaw(JSContext* cx, + Handle<RegExpObject*> regexp, + HandleString input, int32_t lastIndex, + bool* result) { + MOZ_ASSERT(lastIndex >= 0); + MOZ_ASSERT(size_t(lastIndex) <= input->length()); + + VectorMatchPairs matches; + RegExpRunStatus status = + ExecuteRegExp(cx, regexp, input, lastIndex, &matches); + if (status == RegExpRunStatus_Error) { + return false; + } + + *result = (status == RegExpRunStatus_Success); + + RegExpFlags flags = regexp->getFlags(); + if (!flags.global() && !flags.sticky()) { + return true; + } + + int32_t lastIndexNew = *result ? matches[0].limit : 0; + return SetLastIndex<CalledFromJit>(cx, regexp, lastIndexNew); +} + +bool js::RegExpBuiltinExecTestFromJit(JSContext* cx, + Handle<RegExpObject*> regexp, + HandleString input, bool* result) { + int32_t lastIndex = 0; + if (regexp->isGlobalOrSticky()) { + lastIndex = regexp->getLastIndex().toInt32(); + MOZ_ASSERT(lastIndex >= 0); + if (size_t(lastIndex) > input->length()) { + *result = false; + return SetLastIndex<true>(cx, regexp, 0); + } + } + return RegExpBuiltinExecTestRaw<true>(cx, regexp, input, lastIndex, result); +} + +using CapturesVector = GCVector<Value, 4>; + +struct JSSubString { + JSLinearString* base = nullptr; + size_t offset = 0; + size_t length = 0; + + JSSubString() = default; + + void initEmpty(JSLinearString* base) { + this->base = base; + offset = length = 0; + } + void init(JSLinearString* base, size_t offset, size_t length) { + this->base = base; + this->offset = offset; + this->length = length; + } +}; + +static void GetParen(JSLinearString* matched, const JS::Value& capture, + JSSubString* out) { + if (capture.isUndefined()) { + out->initEmpty(matched); + return; + } + JSLinearString& captureLinear = capture.toString()->asLinear(); + out->init(&captureLinear, 0, captureLinear.length()); +} + +template <typename CharT> +static bool InterpretDollar(JSLinearString* matched, JSLinearString* string, + size_t position, size_t tailPos, + Handle<CapturesVector> captures, + Handle<CapturesVector> namedCaptures, + JSLinearString* replacement, + const CharT* replacementBegin, + const CharT* currentDollar, + const CharT* replacementEnd, JSSubString* out, + size_t* skip, uint32_t* currentNamedCapture) { + MOZ_ASSERT(*currentDollar == '$'); + + /* If there is only a dollar, bail now. */ + if (currentDollar + 1 >= replacementEnd) { + return false; + } + + // ES 2021 Table 57: Replacement Text Symbol Substitutions + // https://tc39.es/ecma262/#table-replacement-text-symbol-substitutions + char16_t c = currentDollar[1]; + if (IsAsciiDigit(c)) { + /* $n, $nn */ + unsigned num = AsciiDigitToNumber(c); + if (num > captures.length()) { + // The result is implementation-defined. Do not substitute. + return false; + } + + const CharT* currentChar = currentDollar + 2; + if (currentChar < replacementEnd) { + c = *currentChar; + if (IsAsciiDigit(c)) { + unsigned tmpNum = 10 * num + AsciiDigitToNumber(c); + // If num > captures.length(), the result is implementation-defined. + // Consume next character only if num <= captures.length(). + if (tmpNum <= captures.length()) { + currentChar++; + num = tmpNum; + } + } + } + + if (num == 0) { + // The result is implementation-defined. Do not substitute. + return false; + } + + *skip = currentChar - currentDollar; + + MOZ_ASSERT(num <= captures.length()); + + GetParen(matched, captures[num - 1], out); + return true; + } + + // '$<': Named Captures + if (c == '<') { + // Step 1. + if (namedCaptures.length() == 0) { + return false; + } + + // Step 2.b + const CharT* nameStart = currentDollar + 2; + const CharT* nameEnd = js_strchr_limit(nameStart, '>', replacementEnd); + + // Step 2.c + if (!nameEnd) { + return false; + } + + // Step 2.d + // We precompute named capture replacements in InitNamedCaptures. + // They are stored in the order in which we will need them, so here + // we can just take the next one in the list. + size_t nameLength = nameEnd - nameStart; + *skip = nameLength + 3; // $<...> + + // Steps 2.d.iii-iv + GetParen(matched, namedCaptures[*currentNamedCapture], out); + *currentNamedCapture += 1; + return true; + } + + switch (c) { + default: + return false; + case '$': + out->init(replacement, currentDollar - replacementBegin, 1); + break; + case '&': + out->init(matched, 0, matched->length()); + break; + case '`': + out->init(string, 0, position); + break; + case '\'': + if (tailPos >= string->length()) { + out->initEmpty(matched); + } else { + out->init(string, tailPos, string->length() - tailPos); + } + break; + } + + *skip = 2; + return true; +} + +template <typename CharT> +static bool FindReplaceLengthString(JSContext* cx, + Handle<JSLinearString*> matched, + Handle<JSLinearString*> string, + size_t position, size_t tailPos, + Handle<CapturesVector> captures, + Handle<CapturesVector> namedCaptures, + Handle<JSLinearString*> replacement, + size_t firstDollarIndex, size_t* sizep) { + CheckedInt<uint32_t> replen = replacement->length(); + + JS::AutoCheckCannotGC nogc; + MOZ_ASSERT(firstDollarIndex < replacement->length()); + const CharT* replacementBegin = replacement->chars<CharT>(nogc); + const CharT* currentDollar = replacementBegin + firstDollarIndex; + const CharT* replacementEnd = replacementBegin + replacement->length(); + uint32_t currentNamedCapture = 0; + do { + JSSubString sub; + size_t skip; + if (InterpretDollar(matched, string, position, tailPos, captures, + namedCaptures, replacement, replacementBegin, + currentDollar, replacementEnd, &sub, &skip, + ¤tNamedCapture)) { + if (sub.length > skip) { + replen += sub.length - skip; + } else { + replen -= skip - sub.length; + } + currentDollar += skip; + } else { + currentDollar++; + } + + currentDollar = js_strchr_limit(currentDollar, '$', replacementEnd); + } while (currentDollar); + + if (!replen.isValid()) { + ReportAllocationOverflow(cx); + return false; + } + + *sizep = replen.value(); + return true; +} + +static bool FindReplaceLength(JSContext* cx, Handle<JSLinearString*> matched, + Handle<JSLinearString*> string, size_t position, + size_t tailPos, Handle<CapturesVector> captures, + Handle<CapturesVector> namedCaptures, + Handle<JSLinearString*> replacement, + size_t firstDollarIndex, size_t* sizep) { + return replacement->hasLatin1Chars() + ? FindReplaceLengthString<Latin1Char>( + cx, matched, string, position, tailPos, captures, + namedCaptures, replacement, firstDollarIndex, sizep) + : FindReplaceLengthString<char16_t>( + cx, matched, string, position, tailPos, captures, + namedCaptures, replacement, firstDollarIndex, sizep); +} + +/* + * Precondition: |sb| already has necessary growth space reserved (as + * derived from FindReplaceLength), and has been inflated to TwoByte if + * necessary. + */ +template <typename CharT> +static void DoReplace(Handle<JSLinearString*> matched, + Handle<JSLinearString*> string, size_t position, + size_t tailPos, Handle<CapturesVector> captures, + Handle<CapturesVector> namedCaptures, + Handle<JSLinearString*> replacement, + size_t firstDollarIndex, StringBuffer& sb) { + JS::AutoCheckCannotGC nogc; + const CharT* replacementBegin = replacement->chars<CharT>(nogc); + const CharT* currentChar = replacementBegin; + + MOZ_ASSERT(firstDollarIndex < replacement->length()); + const CharT* currentDollar = replacementBegin + firstDollarIndex; + const CharT* replacementEnd = replacementBegin + replacement->length(); + uint32_t currentNamedCapture = 0; + do { + /* Move one of the constant portions of the replacement value. */ + size_t len = currentDollar - currentChar; + sb.infallibleAppend(currentChar, len); + currentChar = currentDollar; + + JSSubString sub; + size_t skip; + if (InterpretDollar(matched, string, position, tailPos, captures, + namedCaptures, replacement, replacementBegin, + currentDollar, replacementEnd, &sub, &skip, + ¤tNamedCapture)) { + sb.infallibleAppendSubstring(sub.base, sub.offset, sub.length); + currentChar += skip; + currentDollar += skip; + } else { + currentDollar++; + } + + currentDollar = js_strchr_limit(currentDollar, '$', replacementEnd); + } while (currentDollar); + sb.infallibleAppend(currentChar, + replacement->length() - (currentChar - replacementBegin)); +} + +/* + * This function finds the list of named captures of the form + * "$<name>" in a replacement string and converts them into jsids, for + * use in InitNamedReplacements. + */ +template <typename CharT> +static bool CollectNames(JSContext* cx, Handle<JSLinearString*> replacement, + size_t firstDollarIndex, + MutableHandle<GCVector<jsid>> names) { + JS::AutoCheckCannotGC nogc; + MOZ_ASSERT(firstDollarIndex < replacement->length()); + + const CharT* replacementBegin = replacement->chars<CharT>(nogc); + const CharT* currentDollar = replacementBegin + firstDollarIndex; + const CharT* replacementEnd = replacementBegin + replacement->length(); + + // https://tc39.es/ecma262/#table-45, "$<" section + while (currentDollar && currentDollar + 1 < replacementEnd) { + if (currentDollar[1] == '<') { + // Step 2.b + const CharT* nameStart = currentDollar + 2; + const CharT* nameEnd = js_strchr_limit(nameStart, '>', replacementEnd); + + // Step 2.c + if (!nameEnd) { + return true; + } + + // Step 2.d.i + size_t nameLength = nameEnd - nameStart; + JSAtom* atom = AtomizeChars(cx, nameStart, nameLength); + if (!atom || !names.append(AtomToId(atom))) { + return false; + } + currentDollar = nameEnd + 1; + } else { + currentDollar += 2; + } + currentDollar = js_strchr_limit(currentDollar, '$', replacementEnd); + } + return true; +} + +/* + * When replacing named captures, the spec requires us to perform + * `Get(match.groups, name)` for each "$<name>". These `Get`s can be + * script-visible; for example, RegExp can be extended with an `exec` + * method that wraps `groups` in a proxy. To make sure that we do the + * right thing, if a regexp has named captures, we find the named + * capture replacements before beginning the actual replacement. + * This guarantees that we will call GetProperty once and only once for + * each "$<name>" in the replacement string, in the correct order. + * + * This function precomputes the results of step 2 of the '$<' case + * here: https://tc39.es/proposal-regexp-named-groups/#table-45, so + * that when we need to access the nth named capture in InterpretDollar, + * we can just use the nth value stored in namedCaptures. + */ +static bool InitNamedCaptures(JSContext* cx, + Handle<JSLinearString*> replacement, + HandleObject groups, size_t firstDollarIndex, + MutableHandle<CapturesVector> namedCaptures) { + Rooted<GCVector<jsid>> names(cx, cx); + if (replacement->hasLatin1Chars()) { + if (!CollectNames<Latin1Char>(cx, replacement, firstDollarIndex, &names)) { + return false; + } + } else { + if (!CollectNames<char16_t>(cx, replacement, firstDollarIndex, &names)) { + return false; + } + } + + // https://tc39.es/ecma262/#table-45, "$<" section + RootedId id(cx); + RootedValue capture(cx); + for (uint32_t i = 0; i < names.length(); i++) { + // Step 2.d.i + id = names[i]; + + // Step 2.d.ii + if (!GetProperty(cx, groups, groups, id, &capture)) { + return false; + } + + // Step 2.d.iii + if (capture.isUndefined()) { + if (!namedCaptures.append(capture)) { + return false; + } + } else { + // Step 2.d.iv + JSString* str = ToString<CanGC>(cx, capture); + if (!str) { + return false; + } + JSLinearString* linear = str->ensureLinear(cx); + if (!linear) { + return false; + } + if (!namedCaptures.append(StringValue(linear))) { + return false; + } + } + } + + return true; +} + +static bool NeedTwoBytes(Handle<JSLinearString*> string, + Handle<JSLinearString*> replacement, + Handle<JSLinearString*> matched, + Handle<CapturesVector> captures, + Handle<CapturesVector> namedCaptures) { + if (string->hasTwoByteChars()) { + return true; + } + if (replacement->hasTwoByteChars()) { + return true; + } + if (matched->hasTwoByteChars()) { + return true; + } + + for (const Value& capture : captures) { + if (capture.isUndefined()) { + continue; + } + if (capture.toString()->hasTwoByteChars()) { + return true; + } + } + + for (const Value& capture : namedCaptures) { + if (capture.isUndefined()) { + continue; + } + if (capture.toString()->hasTwoByteChars()) { + return true; + } + } + + return false; +} + +// ES2024 draft rev d4927f9bc3706484c75dfef4bbcf5ba826d2632e +// +// 22.2.7.2 RegExpBuiltinExec ( R, S ) +// https://tc39.es/ecma262/#sec-regexpbuiltinexec +// +// If `forTest` is true, this is called from `RegExp.prototype.test` and we can +// avoid allocating a result object. +bool js::RegExpBuiltinExec(JSContext* cx, Handle<RegExpObject*> regexp, + Handle<JSString*> string, bool forTest, + MutableHandle<Value> rval) { + // Step 2. + uint64_t lastIndex; + if (MOZ_LIKELY(regexp->getLastIndex().isInt32())) { + lastIndex = std::max(regexp->getLastIndex().toInt32(), 0); + } else { + Rooted<Value> lastIndexVal(cx, regexp->getLastIndex()); + if (!ToLength(cx, lastIndexVal, &lastIndex)) { + return false; + } + } + + // Steps 3-5. + bool globalOrSticky = regexp->isGlobalOrSticky(); + + // Step 7. + if (!globalOrSticky) { + lastIndex = 0; + } else { + // Steps 1, 13.a. + if (lastIndex > string->length()) { + if (!SetLastIndex(cx, regexp, 0)) { + return false; + } + rval.set(forTest ? BooleanValue(false) : NullValue()); + return true; + } + } + + MOZ_ASSERT(lastIndex <= string->length()); + static_assert(JSString::MAX_LENGTH <= INT32_MAX, "lastIndex fits in int32_t"); + + // Steps 6, 8-35. + + if (forTest) { + bool result; + if (!RegExpBuiltinExecTestRaw<false>(cx, regexp, string, int32_t(lastIndex), + &result)) { + return false; + } + rval.setBoolean(result); + return true; + } + + return RegExpBuiltinExecMatchRaw<false>(cx, regexp, string, + int32_t(lastIndex), nullptr, rval); +} + +// ES2024 draft rev d4927f9bc3706484c75dfef4bbcf5ba826d2632e +// +// 22.2.7.1 RegExpExec ( R, S ) +// https://tc39.es/ecma262/#sec-regexpexec +// +// If `forTest` is true, this is called from `RegExp.prototype.test` and we can +// avoid allocating a result object. +bool js::RegExpExec(JSContext* cx, Handle<JSObject*> regexp, + Handle<JSString*> string, bool forTest, + MutableHandle<Value> rval) { + // Step 1. + Rooted<Value> exec(cx); + Rooted<PropertyKey> execKey(cx, PropertyKey::NonIntAtom(cx->names().exec)); + if (!GetProperty(cx, regexp, regexp, execKey, &exec)) { + return false; + } + + // Step 2. + // If exec is the original RegExp.prototype.exec, use the same, faster, + // path as for the case where exec isn't callable. + PropertyName* execName = cx->names().RegExp_prototype_Exec; + if (MOZ_LIKELY(IsSelfHostedFunctionWithName(exec, execName)) || + !IsCallable(exec)) { + // Steps 3-4. + if (MOZ_LIKELY(regexp->is<RegExpObject>())) { + return RegExpBuiltinExec(cx, regexp.as<RegExpObject>(), string, forTest, + rval); + } + + // Throw an exception if it's not a wrapped RegExpObject that we can safely + // unwrap. + if (!regexp->canUnwrapAs<RegExpObject>()) { + Rooted<Value> thisv(cx, ObjectValue(*regexp)); + return ReportIncompatibleSelfHostedMethod(cx, thisv); + } + + // Call RegExpBuiltinExec in the regular expression's realm. + Rooted<RegExpObject*> unwrapped(cx, ®exp->unwrapAs<RegExpObject>()); + { + AutoRealm ar(cx, unwrapped); + Rooted<JSString*> wrappedString(cx, string); + if (!cx->compartment()->wrap(cx, &wrappedString)) { + return false; + } + if (!RegExpBuiltinExec(cx, unwrapped, wrappedString, forTest, rval)) { + return false; + } + } + return cx->compartment()->wrap(cx, rval); + } + + // Step 2.a. + Rooted<Value> thisv(cx, ObjectValue(*regexp)); + FixedInvokeArgs<1> args(cx); + args[0].setString(string); + if (!js::Call(cx, exec, thisv, args, rval, CallReason::CallContent)) { + return false; + } + + // Step 2.b. + if (!rval.isObjectOrNull()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_EXEC_NOT_OBJORNULL); + return false; + } + + // Step 2.c. + if (forTest) { + rval.setBoolean(rval.isObject()); + } + return true; +} + +/* ES 2021 21.1.3.17.1 */ +// https://tc39.es/ecma262/#sec-getsubstitution +bool js::RegExpGetSubstitution(JSContext* cx, Handle<ArrayObject*> matchResult, + Handle<JSLinearString*> string, size_t position, + Handle<JSLinearString*> replacement, + size_t firstDollarIndex, HandleValue groups, + MutableHandleValue rval) { + MOZ_ASSERT(firstDollarIndex < replacement->length()); + + // Step 1 (skipped). + + // Step 10 (reordered). + uint32_t matchResultLength = matchResult->length(); + MOZ_ASSERT(matchResultLength > 0); + MOZ_ASSERT(matchResultLength == matchResult->getDenseInitializedLength()); + + const Value& matchedValue = matchResult->getDenseElement(0); + Rooted<JSLinearString*> matched(cx, + matchedValue.toString()->ensureLinear(cx)); + if (!matched) { + return false; + } + + // Step 2. + size_t matchLength = matched->length(); + + // Steps 3-5 (skipped). + + // Step 6. + MOZ_ASSERT(position <= string->length()); + + uint32_t nCaptures = matchResultLength - 1; + Rooted<CapturesVector> captures(cx, CapturesVector(cx)); + if (!captures.reserve(nCaptures)) { + return false; + } + + // Step 7. + for (uint32_t i = 1; i <= nCaptures; i++) { + const Value& capture = matchResult->getDenseElement(i); + + if (capture.isUndefined()) { + captures.infallibleAppend(capture); + continue; + } + + JSLinearString* captureLinear = capture.toString()->ensureLinear(cx); + if (!captureLinear) { + return false; + } + captures.infallibleAppend(StringValue(captureLinear)); + } + + Rooted<CapturesVector> namedCaptures(cx, cx); + if (groups.isObject()) { + RootedObject groupsObj(cx, &groups.toObject()); + if (!InitNamedCaptures(cx, replacement, groupsObj, firstDollarIndex, + &namedCaptures)) { + return false; + } + } else { + MOZ_ASSERT(groups.isUndefined()); + } + + // Step 8 (skipped). + + // Step 9. + CheckedInt<uint32_t> checkedTailPos(0); + checkedTailPos += position; + checkedTailPos += matchLength; + if (!checkedTailPos.isValid()) { + ReportAllocationOverflow(cx); + return false; + } + uint32_t tailPos = checkedTailPos.value(); + + // Step 11. + size_t reserveLength; + if (!FindReplaceLength(cx, matched, string, position, tailPos, captures, + namedCaptures, replacement, firstDollarIndex, + &reserveLength)) { + return false; + } + + JSStringBuilder result(cx); + if (NeedTwoBytes(string, replacement, matched, captures, namedCaptures)) { + if (!result.ensureTwoByteChars()) { + return false; + } + } + + if (!result.reserve(reserveLength)) { + return false; + } + + if (replacement->hasLatin1Chars()) { + DoReplace<Latin1Char>(matched, string, position, tailPos, captures, + namedCaptures, replacement, firstDollarIndex, result); + } else { + DoReplace<char16_t>(matched, string, position, tailPos, captures, + namedCaptures, replacement, firstDollarIndex, result); + } + + // Step 12. + JSString* resultString = result.finishString(); + if (!resultString) { + return false; + } + + rval.setString(resultString); + return true; +} + +bool js::GetFirstDollarIndex(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + JSString* str = args[0].toString(); + + // Should be handled in different path. + MOZ_ASSERT(str->length() != 0); + + int32_t index = -1; + if (!GetFirstDollarIndexRaw(cx, str, &index)) { + return false; + } + + args.rval().setInt32(index); + return true; +} + +template <typename TextChar> +static MOZ_ALWAYS_INLINE int GetFirstDollarIndexImpl(const TextChar* text, + uint32_t textLen) { + const TextChar* end = text + textLen; + for (const TextChar* c = text; c != end; ++c) { + if (*c == '$') { + return c - text; + } + } + return -1; +} + +int32_t js::GetFirstDollarIndexRawFlat(JSLinearString* text) { + uint32_t len = text->length(); + + JS::AutoCheckCannotGC nogc; + if (text->hasLatin1Chars()) { + return GetFirstDollarIndexImpl(text->latin1Chars(nogc), len); + } + + return GetFirstDollarIndexImpl(text->twoByteChars(nogc), len); +} + +bool js::GetFirstDollarIndexRaw(JSContext* cx, JSString* str, int32_t* index) { + JSLinearString* text = str->ensureLinear(cx); + if (!text) { + return false; + } + + *index = GetFirstDollarIndexRawFlat(text); + return true; +} + +bool js::RegExpPrototypeOptimizable(JSContext* cx, unsigned argc, Value* vp) { + // This can only be called from self-hosted code. + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + args.rval().setBoolean( + RegExpPrototypeOptimizableRaw(cx, &args[0].toObject())); + return true; +} + +bool js::RegExpPrototypeOptimizableRaw(JSContext* cx, JSObject* proto) { + AutoUnsafeCallWithABI unsafe; + AutoAssertNoPendingException aanpe(cx); + if (!proto->is<NativeObject>()) { + return false; + } + + NativeObject* nproto = static_cast<NativeObject*>(proto); + + Shape* shape = cx->realm()->regExps.getOptimizableRegExpPrototypeShape(); + if (shape == nproto->shape()) { + return true; + } + + JSFunction* flagsGetter; + if (!GetOwnGetterPure(cx, proto, NameToId(cx->names().flags), &flagsGetter)) { + return false; + } + + if (!flagsGetter) { + return false; + } + + if (!IsSelfHostedFunctionWithName(flagsGetter, + cx->names().RegExpFlagsGetter)) { + return false; + } + + JSNative globalGetter; + if (!GetOwnNativeGetterPure(cx, proto, NameToId(cx->names().global), + &globalGetter)) { + return false; + } + + if (globalGetter != regexp_global) { + return false; + } + + JSNative hasIndicesGetter; + if (!GetOwnNativeGetterPure(cx, proto, NameToId(cx->names().hasIndices), + &hasIndicesGetter)) { + return false; + } + + if (hasIndicesGetter != regexp_hasIndices) { + return false; + } + + JSNative ignoreCaseGetter; + if (!GetOwnNativeGetterPure(cx, proto, NameToId(cx->names().ignoreCase), + &ignoreCaseGetter)) { + return false; + } + + if (ignoreCaseGetter != regexp_ignoreCase) { + return false; + } + + JSNative multilineGetter; + if (!GetOwnNativeGetterPure(cx, proto, NameToId(cx->names().multiline), + &multilineGetter)) { + return false; + } + + if (multilineGetter != regexp_multiline) { + return false; + } + + JSNative stickyGetter; + if (!GetOwnNativeGetterPure(cx, proto, NameToId(cx->names().sticky), + &stickyGetter)) { + return false; + } + + if (stickyGetter != regexp_sticky) { + return false; + } + + JSNative unicodeGetter; + if (!GetOwnNativeGetterPure(cx, proto, NameToId(cx->names().unicode), + &unicodeGetter)) { + return false; + } + + if (unicodeGetter != regexp_unicode) { + return false; + } + + JSNative dotAllGetter; + if (!GetOwnNativeGetterPure(cx, proto, NameToId(cx->names().dotAll), + &dotAllGetter)) { + return false; + } + + if (dotAllGetter != regexp_dotAll) { + return false; + } + + // Check if @@match, @@search, and exec are own data properties, + // those values should be tested in selfhosted JS. + bool has = false; + if (!HasOwnDataPropertyPure( + cx, proto, PropertyKey::Symbol(cx->wellKnownSymbols().match), &has)) { + return false; + } + if (!has) { + return false; + } + + if (!HasOwnDataPropertyPure( + cx, proto, PropertyKey::Symbol(cx->wellKnownSymbols().search), + &has)) { + return false; + } + if (!has) { + return false; + } + + if (!HasOwnDataPropertyPure(cx, proto, NameToId(cx->names().exec), &has)) { + return false; + } + if (!has) { + return false; + } + + cx->realm()->regExps.setOptimizableRegExpPrototypeShape(nproto->shape()); + return true; +} + +bool js::RegExpInstanceOptimizable(JSContext* cx, unsigned argc, Value* vp) { + // This can only be called from self-hosted code. + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + + args.rval().setBoolean(RegExpInstanceOptimizableRaw(cx, &args[0].toObject(), + &args[1].toObject())); + return true; +} + +bool js::RegExpInstanceOptimizableRaw(JSContext* cx, JSObject* obj, + JSObject* proto) { + AutoUnsafeCallWithABI unsafe; + AutoAssertNoPendingException aanpe(cx); + + RegExpObject* rx = &obj->as<RegExpObject>(); + + Shape* shape = cx->realm()->regExps.getOptimizableRegExpInstanceShape(); + if (shape == rx->shape()) { + return true; + } + + if (!rx->hasStaticPrototype()) { + return false; + } + + if (rx->staticPrototype() != proto) { + return false; + } + + if (!RegExpObject::isInitialShape(rx)) { + return false; + } + + cx->realm()->regExps.setOptimizableRegExpInstanceShape(rx->shape()); + return true; +} + +/* + * Pattern match the script to check if it is is indexing into a particular + * object, e.g. 'function(a) { return b[a]; }'. Avoid calling the script in + * such cases, which are used by javascript packers (particularly the popular + * Dean Edwards packer) to efficiently encode large scripts. We only handle the + * code patterns generated by such packers here. + */ +bool js::intrinsic_GetElemBaseForLambda(JSContext* cx, unsigned argc, + Value* vp) { + // This can only be called from self-hosted code. + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + JSObject& lambda = args[0].toObject(); + args.rval().setUndefined(); + + if (!lambda.is<JSFunction>()) { + return true; + } + + RootedFunction fun(cx, &lambda.as<JSFunction>()); + if (!fun->isInterpreted() || fun->isClassConstructor()) { + return true; + } + + JSScript* script = JSFunction::getOrCreateScript(cx, fun); + if (!script) { + return false; + } + + jsbytecode* pc = script->code(); + + /* + * JSOp::GetAliasedVar tells us exactly where to find the base object 'b'. + * Rule out the (unlikely) possibility of a function with environment + * objects since it would make our environment walk off. + */ + if (JSOp(*pc) != JSOp::GetAliasedVar || fun->needsSomeEnvironmentObject()) { + return true; + } + EnvironmentCoordinate ec(pc); + EnvironmentObject* env = &fun->environment()->as<EnvironmentObject>(); + for (unsigned i = 0; i < ec.hops(); ++i) { + env = &env->enclosingEnvironment().as<EnvironmentObject>(); + } + Value b = env->aliasedBinding(ec); + pc += JSOpLength_GetAliasedVar; + + /* Look for 'a' to be the lambda's first argument. */ + if (JSOp(*pc) != JSOp::GetArg || GET_ARGNO(pc) != 0) { + return true; + } + pc += JSOpLength_GetArg; + + /* 'b[a]' */ + if (JSOp(*pc) != JSOp::GetElem) { + return true; + } + pc += JSOpLength_GetElem; + + /* 'return b[a]' */ + if (JSOp(*pc) != JSOp::Return) { + return true; + } + + /* 'b' must behave like a normal object. */ + if (!b.isObject()) { + return true; + } + + JSObject& bobj = b.toObject(); + const JSClass* clasp = bobj.getClass(); + if (!clasp->isNativeObject() || clasp->getOpsLookupProperty() || + clasp->getOpsGetProperty()) { + return true; + } + + args.rval().setObject(bobj); + return true; +} + +/* + * Emulates `b[a]` property access, that is detected in GetElemBaseForLambda. + * It returns the property value only if the property is data property and the + * property value is a string. Otherwise it returns undefined. + */ +bool js::intrinsic_GetStringDataProperty(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + + RootedObject obj(cx, &args[0].toObject()); + if (!obj->is<NativeObject>()) { + // The object is already checked to be native in GetElemBaseForLambda, + // but it can be swapped to another class that is non-native. + // Return undefined to mark failure to get the property. + args.rval().setUndefined(); + return true; + } + + JSAtom* atom = AtomizeString(cx, args[1].toString()); + if (!atom) { + return false; + } + + Value v; + if (GetPropertyPure(cx, obj, AtomToId(atom), &v) && v.isString()) { + args.rval().set(v); + } else { + args.rval().setUndefined(); + } + + return true; +} diff --git a/js/src/builtin/RegExp.h b/js/src/builtin/RegExp.h new file mode 100644 index 0000000000..580c997aed --- /dev/null +++ b/js/src/builtin/RegExp.h @@ -0,0 +1,178 @@ +/* -*- 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 builtin_RegExp_h +#define builtin_RegExp_h + +#include <stddef.h> +#include <stdint.h> + +#include "NamespaceImports.h" + +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "vm/RegExpShared.h" + +class JSLinearString; + +namespace JS { +class Value; +} + +/* + * The following builtin natives are extern'd for pointer comparison in + * other parts of the engine. + */ + +namespace js { + +class ArrayObject; +class MatchPairs; +class RegExpObject; +class RegExpStatics; + +JSObject* InitRegExpClass(JSContext* cx, HandleObject obj); + +/* + * Legacy behavior of ExecuteRegExp(), which is baked into the JSAPI. + * + * |res| may be nullptr if the RegExpStatics are not to be updated. + * |input| may be nullptr if there is no JSString corresponding to + * |chars| and |length|. + */ +[[nodiscard]] bool ExecuteRegExpLegacy(JSContext* cx, RegExpStatics* res, + Handle<RegExpObject*> reobj, + Handle<JSLinearString*> input, + size_t* lastIndex, bool test, + MutableHandleValue rval); + +// Translation from MatchPairs to a JS array in regexp_exec()'s output format. +[[nodiscard]] bool CreateRegExpMatchResult(JSContext* cx, HandleRegExpShared re, + HandleString input, + const MatchPairs& matches, + MutableHandleValue rval); + +[[nodiscard]] extern bool RegExpMatcher(JSContext* cx, unsigned argc, + Value* vp); + +[[nodiscard]] extern bool RegExpMatcherRaw(JSContext* cx, HandleObject regexp, + HandleString input, + int32_t lastIndex, + MatchPairs* maybeMatches, + MutableHandleValue output); + +[[nodiscard]] extern bool RegExpSearcher(JSContext* cx, unsigned argc, + Value* vp); + +[[nodiscard]] extern bool RegExpSearcherRaw(JSContext* cx, HandleObject regexp, + HandleString input, + int32_t lastIndex, + MatchPairs* maybeMatches, + int32_t* result); + +[[nodiscard]] extern bool RegExpBuiltinExecMatchFromJit( + JSContext* cx, Handle<RegExpObject*> regexp, HandleString input, + MatchPairs* maybeMatches, MutableHandleValue output); + +[[nodiscard]] extern bool RegExpBuiltinExecTestFromJit( + JSContext* cx, Handle<RegExpObject*> regexp, HandleString input, + bool* result); + +[[nodiscard]] extern bool intrinsic_GetElemBaseForLambda(JSContext* cx, + unsigned argc, + Value* vp); + +[[nodiscard]] extern bool intrinsic_GetStringDataProperty(JSContext* cx, + unsigned argc, + Value* vp); + +/* + * The following functions are for use by self-hosted code. + */ + +/* + * Behaves like RegExp(source, flags). + * |source| must be a valid regular expression pattern, |flags| is a raw + * integer value representing the regular expression flags. + * Must be called without |new|. + * + * Dedicated function for RegExp.prototype[@@replace] and + * RegExp.prototype[@@split] optimized paths. + */ +[[nodiscard]] extern bool regexp_construct_raw_flags(JSContext* cx, + unsigned argc, Value* vp); + +[[nodiscard]] extern bool IsRegExp(JSContext* cx, HandleValue value, + bool* result); + +[[nodiscard]] extern bool RegExpCreate(JSContext* cx, HandleValue pattern, + HandleValue flags, + MutableHandleValue rval); + +[[nodiscard]] extern bool RegExpPrototypeOptimizable(JSContext* cx, + unsigned argc, Value* vp); + +[[nodiscard]] extern bool RegExpPrototypeOptimizableRaw(JSContext* cx, + JSObject* proto); + +[[nodiscard]] extern bool RegExpInstanceOptimizable(JSContext* cx, + unsigned argc, Value* vp); + +[[nodiscard]] extern bool RegExpInstanceOptimizableRaw(JSContext* cx, + JSObject* obj, + JSObject* proto); + +[[nodiscard]] extern bool RegExpBuiltinExec(JSContext* cx, + Handle<RegExpObject*> regexp, + Handle<JSString*> string, + bool forTest, + MutableHandle<Value> rval); + +[[nodiscard]] extern bool RegExpExec(JSContext* cx, Handle<JSObject*> regexp, + Handle<JSString*> string, bool forTest, + MutableHandle<Value> rval); + +[[nodiscard]] extern bool RegExpGetSubstitution( + JSContext* cx, Handle<ArrayObject*> matchResult, + Handle<JSLinearString*> string, size_t position, + Handle<JSLinearString*> replacement, size_t firstDollarIndex, + HandleValue namedCaptures, MutableHandleValue rval); + +[[nodiscard]] extern bool GetFirstDollarIndex(JSContext* cx, unsigned argc, + Value* vp); + +[[nodiscard]] extern bool GetFirstDollarIndexRaw(JSContext* cx, JSString* str, + int32_t* index); + +extern int32_t GetFirstDollarIndexRawFlat(JSLinearString* text); + +// RegExp ClassSpec members used in RegExpObject.cpp. +[[nodiscard]] extern bool regexp_construct(JSContext* cx, unsigned argc, + Value* vp); +extern const JSPropertySpec regexp_static_props[]; +extern const JSPropertySpec regexp_properties[]; +extern const JSFunctionSpec regexp_methods[]; + +// Used in RegExpObject::isOriginalFlagGetter. +[[nodiscard]] extern bool regexp_hasIndices(JSContext* cx, unsigned argc, + JS::Value* vp); +[[nodiscard]] extern bool regexp_global(JSContext* cx, unsigned argc, + JS::Value* vp); +[[nodiscard]] extern bool regexp_ignoreCase(JSContext* cx, unsigned argc, + JS::Value* vp); +[[nodiscard]] extern bool regexp_multiline(JSContext* cx, unsigned argc, + JS::Value* vp); +[[nodiscard]] extern bool regexp_dotAll(JSContext* cx, unsigned argc, + JS::Value* vp); +[[nodiscard]] extern bool regexp_sticky(JSContext* cx, unsigned argc, + JS::Value* vp); +[[nodiscard]] extern bool regexp_unicode(JSContext* cx, unsigned argc, + JS::Value* vp); + +} /* namespace js */ + +#endif /* builtin_RegExp_h */ diff --git a/js/src/builtin/RegExp.js b/js/src/builtin/RegExp.js new file mode 100644 index 0000000000..e19dfc2b3d --- /dev/null +++ b/js/src/builtin/RegExp.js @@ -0,0 +1,1574 @@ +/* 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/. */ + +// ECMAScript 2020 draft (2020/03/12) 21.2.5.4 get RegExp.prototype.flags +// https://tc39.es/ecma262/#sec-get-regexp.prototype.flags +// Uncloned functions with `$` prefix are allocated as extended function +// to store the original name in `SetCanonicalName`. +function $RegExpFlagsGetter() { + // Steps 1-2. + var R = this; + if (!IsObject(R)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, R === null ? "null" : typeof R); + } + + // Step 3. + var result = ""; + + // Steps 4-5. + if (R.hasIndices) { + result += "d"; + } + + // Steps 6-7. + if (R.global) { + result += "g"; + } + + // Steps 8-9. + if (R.ignoreCase) { + result += "i"; + } + + // Steps 10-11. + if (R.multiline) { + result += "m"; + } + + // Steps 12-13. + if (R.dotAll) { + result += "s"; + } + + // Steps 14-15. + if (R.unicode) { + result += "u"; + } + + // Steps 16-17 + if (R.sticky) { + result += "y"; + } + + // Step 18. + return result; +} +SetCanonicalName($RegExpFlagsGetter, "get flags"); + +// ES 2017 draft 40edb3a95a475c1b251141ac681b8793129d9a6d 21.2.5.14. +function $RegExpToString() { + // Step 1. + var R = this; + + // Step 2. + if (!IsObject(R)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, R === null ? "null" : typeof R); + } + + // Step 3. + var pattern = ToString(R.source); + + // Step 4. + var flags = ToString(R.flags); + + // Steps 5-6. + return "/" + pattern + "/" + flags; +} +SetCanonicalName($RegExpToString, "toString"); + +// ES 2016 draft Mar 25, 2016 21.2.5.2.3. +function AdvanceStringIndex(S, index) { + // Step 1. + assert(typeof S === "string", "Expected string as 1st argument"); + + // Step 2. + assert( + index >= 0 && index <= MAX_NUMERIC_INDEX, + "Expected integer as 2nd argument" + ); + + // Step 3 (skipped). + + // Step 4 (skipped). + + // Step 5. + var length = S.length; + + // Step 6. + if (index + 1 >= length) { + return index + 1; + } + + // Step 7. + var first = callFunction(std_String_charCodeAt, S, index); + + // Step 8. + if (first < 0xd800 || first > 0xdbff) { + return index + 1; + } + + // Step 9. + var second = callFunction(std_String_charCodeAt, S, index + 1); + + // Step 10. + if (second < 0xdc00 || second > 0xdfff) { + return index + 1; + } + + // Step 11. + return index + 2; +} + +// ES2023 draft rev 2c78e6f6b5bc6bfbf79dd8a12a9593e5b57afcd2 +// 22.2.5.8 RegExp.prototype [ @@match ] ( string ) +function RegExpMatch(string) { + // Step 1. + var rx = this; + + // Step 2. + if (!IsObject(rx)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, rx === null ? "null" : typeof rx); + } + + // Step 3. + var S = ToString(string); + + // Optimized paths for simple cases. + if (IsRegExpMethodOptimizable(rx)) { + // Step 4. + var flags = UnsafeGetInt32FromReservedSlot(rx, REGEXP_FLAGS_SLOT); + var global = !!(flags & REGEXP_GLOBAL_FLAG); + + if (global) { + // Step 6.a. + var fullUnicode = !!(flags & REGEXP_UNICODE_FLAG); + + // Steps 6.b-e. + return RegExpGlobalMatchOpt(rx, S, fullUnicode); + } + + // Step 5. + return RegExpBuiltinExec(rx, S); + } + + // Stes 4-6 + return RegExpMatchSlowPath(rx, S); +} + +// ES2023 draft rev 2c78e6f6b5bc6bfbf79dd8a12a9593e5b57afcd2 +// 22.2.5.8 RegExp.prototype [ @@match ] ( string ) +// Steps 4-6 +function RegExpMatchSlowPath(rx, S) { + // Step 4. + var flags = ToString(rx.flags); + + // Step 5. + if (!callFunction(std_String_includes, flags, "g")) { + return RegExpExec(rx, S); + } + + // Step 6.a. + var fullUnicode = callFunction(std_String_includes, flags, "u"); + + // Step 6.b. + rx.lastIndex = 0; + + // Step 6.c. + var A = []; + + // Step 6.d. + var n = 0; + + // Step 6.e. + while (true) { + // Step 6.e.i. + var result = RegExpExec(rx, S); + + // Step 6.e.ii. + if (result === null) { + return n === 0 ? null : A; + } + + // Step 6.e.iii.1. + var matchStr = ToString(result[0]); + + // Step 6.e.iii.2. + DefineDataProperty(A, n, matchStr); + + // Step 6.e.iii.3. + if (matchStr === "") { + var lastIndex = ToLength(rx.lastIndex); + rx.lastIndex = fullUnicode + ? AdvanceStringIndex(S, lastIndex) + : lastIndex + 1; + } + + // Step 6.e.iii.4. + n++; + } +} + +// ES 2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e 21.2.5.6. +// Steps 6.b-e. +// Optimized path for @@match with global flag. +function RegExpGlobalMatchOpt(rx, S, fullUnicode) { + // Step 6.b. + var lastIndex = 0; + rx.lastIndex = 0; + + // Step 6.c. + var A = []; + + // Step 6.d. + var n = 0; + + var lengthS = S.length; + + // Step 6.e. + while (true) { + // Step 6.e.i. + var result = RegExpMatcher(rx, S, lastIndex); + + // Step 6.e.ii. + if (result === null) { + return n === 0 ? null : A; + } + + lastIndex = result.index + result[0].length; + + // Step 6.e.iii.1. + var matchStr = result[0]; + + // Step 6.e.iii.2. + DefineDataProperty(A, n, matchStr); + + // Step 6.e.iii.4. + if (matchStr === "") { + lastIndex = fullUnicode + ? AdvanceStringIndex(S, lastIndex) + : lastIndex + 1; + if (lastIndex > lengthS) { + return A; + } + } + + // Step 6.e.iii.5. + n++; + } +} + +// Checks if following properties and getters are not modified, and accessing +// them not observed by content script: +// * flags +// * hasIndices +// * global +// * ignoreCase +// * multiline +// * dotAll +// * sticky +// * unicode +// * exec +// * lastIndex +function IsRegExpMethodOptimizable(rx) { + if (!IsRegExpObject(rx)) { + return false; + } + + var RegExpProto = GetBuiltinPrototype("RegExp"); + // If RegExpPrototypeOptimizable and RegExpInstanceOptimizable succeed, + // `RegExpProto.exec` is guaranteed to be data properties. + return ( + RegExpPrototypeOptimizable(RegExpProto) && + RegExpInstanceOptimizable(rx, RegExpProto) && + RegExpProto.exec === RegExp_prototype_Exec + ); +} + +// ES2023 draft rev 2c78e6f6b5bc6bfbf79dd8a12a9593e5b57afcd2 +// 22.2.5.11 RegExp.prototype [ @@replace ] ( string, replaceValue ) +function RegExpReplace(string, replaceValue) { + // Step 1. + var rx = this; + + // Step 2. + if (!IsObject(rx)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, rx === null ? "null" : typeof rx); + } + + // Step 3. + var S = ToString(string); + + // Step 4. + var lengthS = S.length; + + // Step 5. + var functionalReplace = IsCallable(replaceValue); + + // Step 6. + var firstDollarIndex = -1; + if (!functionalReplace) { + // Step 6.a. + replaceValue = ToString(replaceValue); + + // Skip if replaceValue is an empty string or a single character. + // A single character string may contain "$", but that cannot be a + // substitution. + if (replaceValue.length > 1) { + firstDollarIndex = GetFirstDollarIndex(replaceValue); + } + } + + // Optimized paths. + if (IsRegExpMethodOptimizable(rx)) { + // Step 7. + var flags = UnsafeGetInt32FromReservedSlot(rx, REGEXP_FLAGS_SLOT); + + // Step 9. + var global = !!(flags & REGEXP_GLOBAL_FLAG); + + // Steps 9-17. + if (global) { + if (functionalReplace) { + // For large strings check if the replacer function is + // applicable for the elem-base optimization. + if (lengthS > 5000) { + var elemBase = GetElemBaseForLambda(replaceValue); + if (IsObject(elemBase)) { + return RegExpGlobalReplaceOptElemBase( + rx, + S, + lengthS, + replaceValue, + flags, + elemBase + ); + } + } + return RegExpGlobalReplaceOptFunc(rx, S, lengthS, replaceValue, flags); + } + if (firstDollarIndex !== -1) { + return RegExpGlobalReplaceOptSubst( + rx, + S, + lengthS, + replaceValue, + flags, + firstDollarIndex + ); + } + if (lengthS < 0x7fff) { + return RegExpGlobalReplaceShortOpt(rx, S, lengthS, replaceValue, flags); + } + return RegExpGlobalReplaceOpt(rx, S, lengthS, replaceValue, flags); + } + + if (functionalReplace) { + return RegExpLocalReplaceOptFunc(rx, S, lengthS, replaceValue); + } + if (firstDollarIndex !== -1) { + return RegExpLocalReplaceOptSubst( + rx, + S, + lengthS, + replaceValue, + firstDollarIndex + ); + } + if (lengthS < 0x7fff) { + return RegExpLocalReplaceOptShort(rx, S, lengthS, replaceValue); + } + return RegExpLocalReplaceOpt(rx, S, lengthS, replaceValue); + } + + // Steps 7-17. + return RegExpReplaceSlowPath( + rx, + S, + lengthS, + replaceValue, + functionalReplace, + firstDollarIndex + ); +} + +// ES2023 draft rev 2c78e6f6b5bc6bfbf79dd8a12a9593e5b57afcd2 +// 22.2.5.11 RegExp.prototype [ @@replace ] ( string, replaceValue ) +// Steps 7-17. +// Slow path for @@replace. +function RegExpReplaceSlowPath( + rx, + S, + lengthS, + replaceValue, + functionalReplace, + firstDollarIndex +) { + // Step 7. + var flags = ToString(rx.flags); + + // Step 8. + var global = callFunction(std_String_includes, flags, "g"); + + // Step 9. + var fullUnicode = false; + if (global) { + // Step 9.a. + fullUnicode = callFunction(std_String_includes, flags, "u"); + + // Step 9.b. + rx.lastIndex = 0; + } + + // Step 10. + var results = new_List(); + var nResults = 0; + + // Steps 11-12. + while (true) { + // Step 12.a. + var result = RegExpExec(rx, S); + + // Step 12.b. + if (result === null) { + break; + } + + // Step 12.c.i. + DefineDataProperty(results, nResults++, result); + + // Step 12.c.ii. + if (!global) { + break; + } + + // Step 12.c.iii.1. + var matchStr = ToString(result[0]); + + // Step 12.c.iii.2. + if (matchStr === "") { + var lastIndex = ToLength(rx.lastIndex); + rx.lastIndex = fullUnicode + ? AdvanceStringIndex(S, lastIndex) + : lastIndex + 1; + } + } + + // Step 13. + var accumulatedResult = ""; + + // Step 14. + var nextSourcePosition = 0; + + // Step 15. + for (var i = 0; i < nResults; i++) { + result = results[i]; + + // Steps 15.a-b. + var nCaptures = std_Math_max(ToLength(result.length) - 1, 0); + + // Step 15.c. + var matched = ToString(result[0]); + + // Step 15.d. + var matchLength = matched.length; + + // Steps 15.e-f. + var position = std_Math_max( + std_Math_min(ToInteger(result.index), lengthS), + 0 + ); + + var replacement; + if (functionalReplace || firstDollarIndex !== -1) { + // Steps 15.g-l. + replacement = RegExpGetComplexReplacement( + result, + matched, + S, + position, + nCaptures, + replaceValue, + functionalReplace, + firstDollarIndex + ); + } else { + // Steps 15.g, 15.i, 15.i.iv. + // We don't need captures array, but ToString is visible to script. + for (var n = 1; n <= nCaptures; n++) { + // Steps 15.i.i-ii. + var capN = result[n]; + + // Step 15.i.ii. + if (capN !== undefined) { + ToString(capN); + } + } + + // Steps 15.j, 15.l.i. + // We don't need namedCaptures, but ToObject is visible to script. + var namedCaptures = result.groups; + if (namedCaptures !== undefined) { + ToObject(namedCaptures); + } + + // Step 15.l.ii. + replacement = replaceValue; + } + + // Step 15.m. + if (position >= nextSourcePosition) { + // Step 15.m.ii. + accumulatedResult += + Substring(S, nextSourcePosition, position - nextSourcePosition) + + replacement; + + // Step 15.m.iii. + nextSourcePosition = position + matchLength; + } + } + + // Step 16. + if (nextSourcePosition >= lengthS) { + return accumulatedResult; + } + + // Step 17. + return ( + accumulatedResult + + Substring(S, nextSourcePosition, lengthS - nextSourcePosition) + ); +} + +// ES2023 draft rev 2c78e6f6b5bc6bfbf79dd8a12a9593e5b57afcd2 +// 22.2.5.11 RegExp.prototype [ @@replace ] ( string, replaceValue ) +// https://tc39.es/ecma262/#sec-regexp.prototype-@@replace +// Steps 15.g-l. +// Calculates functional/substitution replacement from match result. +// Used in the following functions: +// * RegExpReplaceSlowPath +function RegExpGetComplexReplacement( + result, + matched, + S, + position, + nCaptures, + replaceValue, + functionalReplace, + firstDollarIndex +) { + // Step 15.g. + var captures = new_List(); + var capturesLength = 0; + + // Step 15.k.i (reordered). + DefineDataProperty(captures, capturesLength++, matched); + + // Steps 15.h, 15.i, 15.i.v. + for (var n = 1; n <= nCaptures; n++) { + // Step 15.i.i. + var capN = result[n]; + + // Step 15.i.ii. + if (capN !== undefined) { + capN = ToString(capN); + } + + // Step 15.i.iii. + DefineDataProperty(captures, capturesLength++, capN); + } + + // Step 15.j. + var namedCaptures = result.groups; + + // Step 15.k. + if (functionalReplace) { + // For `nCaptures` <= 4 case, call `replaceValue` directly, otherwise + // use `std_Function_apply` with all arguments stored in `captures`. + if (namedCaptures === undefined) { + switch (nCaptures) { + case 0: + return ToString( + callContentFunction( + replaceValue, + undefined, + SPREAD(captures, 1), + position, + S + ) + ); + case 1: + return ToString( + callContentFunction( + replaceValue, + undefined, + SPREAD(captures, 2), + position, + S + ) + ); + case 2: + return ToString( + callContentFunction( + replaceValue, + undefined, + SPREAD(captures, 3), + position, + S + ) + ); + case 3: + return ToString( + callContentFunction( + replaceValue, + undefined, + SPREAD(captures, 4), + position, + S + ) + ); + case 4: + return ToString( + callContentFunction( + replaceValue, + undefined, + SPREAD(captures, 5), + position, + S + ) + ); + } + } + + // Steps 15.k.ii-vi. + DefineDataProperty(captures, capturesLength++, position); + DefineDataProperty(captures, capturesLength++, S); + if (namedCaptures !== undefined) { + DefineDataProperty(captures, capturesLength++, namedCaptures); + } + return ToString( + callFunction(std_Function_apply, replaceValue, undefined, captures) + ); + } + + // Step 15.l. + if (namedCaptures !== undefined) { + namedCaptures = ToObject(namedCaptures); + } + return RegExpGetSubstitution( + captures, + S, + position, + replaceValue, + firstDollarIndex, + namedCaptures + ); +} + +// ES2023 draft rev 2c78e6f6b5bc6bfbf79dd8a12a9593e5b57afcd2 +// 22.2.5.11 RegExp.prototype [ @@replace ] ( string, replaceValue ) +// https://tc39.es/ecma262/#sec-regexp.prototype-@@replace +// Steps 15.g-k. +// Calculates functional replacement from match result. +// Used in the following functions: +// * RegExpGlobalReplaceOptFunc +// * RegExpGlobalReplaceOptElemBase +// * RegExpLocalReplaceOptFunc +function RegExpGetFunctionalReplacement(result, S, position, replaceValue) { + // For `nCaptures` <= 4 case, call `replaceValue` directly, otherwise + // use `std_Function_apply` with all arguments stored in `captures`. + assert(result.length >= 1, "RegExpMatcher doesn't return an empty array"); + var nCaptures = result.length - 1; + + // Step 15.j (reordered) + var namedCaptures = result.groups; + + if (namedCaptures === undefined) { + switch (nCaptures) { + case 0: + return ToString( + callContentFunction( + replaceValue, + undefined, + SPREAD(result, 1), + position, + S + ) + ); + case 1: + return ToString( + callContentFunction( + replaceValue, + undefined, + SPREAD(result, 2), + position, + S + ) + ); + case 2: + return ToString( + callContentFunction( + replaceValue, + undefined, + SPREAD(result, 3), + position, + S + ) + ); + case 3: + return ToString( + callContentFunction( + replaceValue, + undefined, + SPREAD(result, 4), + position, + S + ) + ); + case 4: + return ToString( + callContentFunction( + replaceValue, + undefined, + SPREAD(result, 5), + position, + S + ) + ); + } + } + + // Steps 15.g-i, 15.k.i-ii. + var captures = new_List(); + for (var n = 0; n <= nCaptures; n++) { + assert( + typeof result[n] === "string" || result[n] === undefined, + "RegExpMatcher returns only strings and undefined" + ); + DefineDataProperty(captures, n, result[n]); + } + + // Step 15.k.iii. + DefineDataProperty(captures, nCaptures + 1, position); + DefineDataProperty(captures, nCaptures + 2, S); + + // Step 15.k.iv. + if (namedCaptures !== undefined) { + DefineDataProperty(captures, nCaptures + 3, namedCaptures); + } + + // Steps 15.k.v-vi. + return ToString( + callFunction(std_Function_apply, replaceValue, undefined, captures) + ); +} + +// ES2023 draft rev 2c78e6f6b5bc6bfbf79dd8a12a9593e5b57afcd2 +// 22.2.5.11 RegExp.prototype [ @@replace ] ( string, replaceValue ) +// Steps 9.b-17. +// Optimized path for @@replace with the following conditions: +// * global flag is true +// * S is a short string (lengthS < 0x7fff) +// * replaceValue is a string without "$" +function RegExpGlobalReplaceShortOpt(rx, S, lengthS, replaceValue, flags) { + // Step 9.a. + var fullUnicode = !!(flags & REGEXP_UNICODE_FLAG); + + // Step 9.b. + var lastIndex = 0; + rx.lastIndex = 0; + + // Step 13 (reordered). + var accumulatedResult = ""; + + // Step 14 (reordered). + var nextSourcePosition = 0; + + // Step 12. + while (true) { + // Step 12.a. + var result = RegExpSearcher(rx, S, lastIndex); + + // Step 12.b. + if (result === -1) { + break; + } + + var position = result & 0x7fff; + lastIndex = (result >> 15) & 0x7fff; + + // Step 15.m.ii. + accumulatedResult += + Substring(S, nextSourcePosition, position - nextSourcePosition) + + replaceValue; + + // Step 15.m.iii. + nextSourcePosition = lastIndex; + + // Step 12.c.iii.2. + if (lastIndex === position) { + lastIndex = fullUnicode + ? AdvanceStringIndex(S, lastIndex) + : lastIndex + 1; + if (lastIndex > lengthS) { + break; + } + } + } + + // Step 16. + if (nextSourcePosition >= lengthS) { + return accumulatedResult; + } + + // Step 17. + return ( + accumulatedResult + + Substring(S, nextSourcePosition, lengthS - nextSourcePosition) + ); +} + +// ES2023 draft rev 2c78e6f6b5bc6bfbf79dd8a12a9593e5b57afcd2 +// 22.2.5.11 RegExp.prototype [ @@replace ] ( string, replaceValue ) +// Steps 7-17. +// Optimized path for @@replace. + +// Conditions: +// * global flag is true +// * replaceValue is a string without "$" +#define FUNC_NAME RegExpGlobalReplaceOpt +#include "RegExpGlobalReplaceOpt.h.js" +#undef FUNC_NAME +/* global RegExpGlobalReplaceOpt */ + +// Conditions: +// * global flag is true +// * replaceValue is a function +#define FUNC_NAME RegExpGlobalReplaceOptFunc +#define FUNCTIONAL +#include "RegExpGlobalReplaceOpt.h.js" +#undef FUNCTIONAL +#undef FUNC_NAME +/* global RegExpGlobalReplaceOptFunc */ + +// Conditions: +// * global flag is true +// * replaceValue is a function that returns element of an object +#define FUNC_NAME RegExpGlobalReplaceOptElemBase +#define ELEMBASE +#include "RegExpGlobalReplaceOpt.h.js" +#undef ELEMBASE +#undef FUNC_NAME +/* global RegExpGlobalReplaceOptElemBase */ + +// Conditions: +// * global flag is true +// * replaceValue is a string with "$" +#define FUNC_NAME RegExpGlobalReplaceOptSubst +#define SUBSTITUTION +#include "RegExpGlobalReplaceOpt.h.js" +#undef SUBSTITUTION +#undef FUNC_NAME +/* global RegExpGlobalReplaceOptSubst */ + +// Conditions: +// * global flag is false +// * replaceValue is a string without "$" +#define FUNC_NAME RegExpLocalReplaceOpt +#include "RegExpLocalReplaceOpt.h.js" +#undef FUNC_NAME +/* global RegExpLocalReplaceOpt */ + +// Conditions: +// * global flag is false +// * S is a short string (lengthS < 0x7fff) +// * replaceValue is a string without "$" +#define FUNC_NAME RegExpLocalReplaceOptShort +#define SHORT_STRING +#include "RegExpLocalReplaceOpt.h.js" +#undef SHORT_STRING +#undef FUNC_NAME +/* global RegExpLocalReplaceOptShort */ + +// Conditions: +// * global flag is false +// * replaceValue is a function +#define FUNC_NAME RegExpLocalReplaceOptFunc +#define FUNCTIONAL +#include "RegExpLocalReplaceOpt.h.js" +#undef FUNCTIONAL +#undef FUNC_NAME +/* global RegExpLocalReplaceOptFunc */ + +// Conditions: +// * global flag is false +// * replaceValue is a string with "$" +#define FUNC_NAME RegExpLocalReplaceOptSubst +#define SUBSTITUTION +#include "RegExpLocalReplaceOpt.h.js" +#undef SUBSTITUTION +#undef FUNC_NAME +/* global RegExpLocalReplaceOptSubst */ + +// ES2017 draft rev 6390c2f1b34b309895d31d8c0512eac8660a0210 +// 21.2.5.9 RegExp.prototype [ @@search ] ( string ) +function RegExpSearch(string) { + // Step 1. + var rx = this; + + // Step 2. + if (!IsObject(rx)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, rx === null ? "null" : typeof rx); + } + + // Step 3. + var S = ToString(string); + + // Step 4. + var previousLastIndex = rx.lastIndex; + + // Step 5. + var lastIndexIsZero = SameValue(previousLastIndex, 0); + if (!lastIndexIsZero) { + rx.lastIndex = 0; + } + + if (IsRegExpMethodOptimizable(rx) && S.length < 0x7fff) { + // Step 6. + var result = RegExpSearcher(rx, S, 0); + + // We need to consider two cases: + // + // 1. Neither global nor sticky is set: + // RegExpBuiltinExec doesn't modify lastIndex for local RegExps, that + // means |SameValue(rx.lastIndex, 0)| is true after calling exec. The + // comparison in steps 7-8 |SameValue(rx.lastIndex, previousLastIndex)| + // is therefore equal to the already computed |lastIndexIsZero| value. + // + // 2. Global or sticky flag is set. + // RegExpBuiltinExec will always update lastIndex and we need to + // restore the property to its original value. + + // Steps 7-8. + if (!lastIndexIsZero) { + rx.lastIndex = previousLastIndex; + } else { + var flags = UnsafeGetInt32FromReservedSlot(rx, REGEXP_FLAGS_SLOT); + if (flags & (REGEXP_GLOBAL_FLAG | REGEXP_STICKY_FLAG)) { + rx.lastIndex = previousLastIndex; + } + } + + // Step 9. + if (result === -1) { + return -1; + } + + // Step 10. + return result & 0x7fff; + } + + return RegExpSearchSlowPath(rx, S, previousLastIndex); +} + +// ES2017 draft rev 6390c2f1b34b309895d31d8c0512eac8660a0210 +// 21.2.5.9 RegExp.prototype [ @@search ] ( string ) +// Steps 6-10. +function RegExpSearchSlowPath(rx, S, previousLastIndex) { + // Step 6. + var result = RegExpExec(rx, S); + + // Step 7. + var currentLastIndex = rx.lastIndex; + + // Step 8. + if (!SameValue(currentLastIndex, previousLastIndex)) { + rx.lastIndex = previousLastIndex; + } + + // Step 9. + if (result === null) { + return -1; + } + + // Step 10. + return result.index; +} + +function IsRegExpSplitOptimizable(rx, C) { + if (!IsRegExpObject(rx)) { + return false; + } + + var RegExpCtor = GetBuiltinConstructor("RegExp"); + if (C !== RegExpCtor) { + return false; + } + + var RegExpProto = RegExpCtor.prototype; + // If RegExpPrototypeOptimizable succeeds, `RegExpProto.exec` is guaranteed + // to be a data property. + return ( + RegExpPrototypeOptimizable(RegExpProto) && + RegExpInstanceOptimizable(rx, RegExpProto) && + RegExpProto.exec === RegExp_prototype_Exec + ); +} + +// ES 2017 draft 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e 21.2.5.11. +function RegExpSplit(string, limit) { + // Step 1. + var rx = this; + + // Step 2. + if (!IsObject(rx)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, rx === null ? "null" : typeof rx); + } + + // Step 3. + var S = ToString(string); + + // Step 4. + var C = SpeciesConstructor(rx, GetBuiltinConstructor("RegExp")); + + var optimizable = + IsRegExpSplitOptimizable(rx, C) && + (limit === undefined || typeof limit === "number"); + + var flags, unicodeMatching, splitter; + if (optimizable) { + // Step 5. + flags = UnsafeGetInt32FromReservedSlot(rx, REGEXP_FLAGS_SLOT); + + // Steps 6-7. + unicodeMatching = !!(flags & REGEXP_UNICODE_FLAG); + + // Steps 8-10. + // If split operation is optimizable, perform non-sticky match. + if (flags & REGEXP_STICKY_FLAG) { + var source = UnsafeGetStringFromReservedSlot(rx, REGEXP_SOURCE_SLOT); + splitter = RegExpConstructRaw(source, flags & ~REGEXP_STICKY_FLAG); + } else { + splitter = rx; + } + } else { + // Step 5. + flags = ToString(rx.flags); + + // Steps 6-7. + unicodeMatching = callFunction(std_String_includes, flags, "u"); + + // Steps 8-9. + var newFlags; + if (callFunction(std_String_includes, flags, "y")) { + newFlags = flags; + } else { + newFlags = flags + "y"; + } + + // Step 10. + splitter = constructContentFunction(C, C, rx, newFlags); + } + + // Step 11. + var A = []; + + // Step 12. + var lengthA = 0; + + // Step 13. + var lim; + if (limit === undefined) { + lim = MAX_UINT32; + } else { + lim = limit >>> 0; + } + + // Step 15. + var p = 0; + + // Step 16. + if (lim === 0) { + return A; + } + + // Step 14 (reordered). + var size = S.length; + + // Step 17. + if (size === 0) { + // Step 17.a. + var z; + if (optimizable) { + z = RegExpMatcher(splitter, S, 0); + } else { + z = RegExpExec(splitter, S); + } + + // Step 17.b. + if (z !== null) { + return A; + } + + // Step 17.d. + DefineDataProperty(A, 0, S); + + // Step 17.e. + return A; + } + + // Step 18. + var q = p; + + // Step 19. + while (q < size) { + var e; + if (optimizable) { + // Step 19.a (skipped). + // splitter.lastIndex is not used. + + // Step 19.b. + z = RegExpMatcher(splitter, S, q); + + // Step 19.c. + if (z === null) { + break; + } + + // splitter.lastIndex is not updated. + q = z.index; + if (q >= size) { + break; + } + + // Step 19.d.i. + e = q + z[0].length; + } else { + // Step 19.a. + splitter.lastIndex = q; + + // Step 19.b. + z = RegExpExec(splitter, S); + + // Step 19.c. + if (z === null) { + q = unicodeMatching ? AdvanceStringIndex(S, q) : q + 1; + continue; + } + + // Step 19.d.i. + e = ToLength(splitter.lastIndex); + } + + // Step 19.d.iii. + if (e === p) { + q = unicodeMatching ? AdvanceStringIndex(S, q) : q + 1; + continue; + } + + // Steps 19.d.iv.1-3. + DefineDataProperty(A, lengthA, Substring(S, p, q - p)); + + // Step 19.d.iv.4. + lengthA++; + + // Step 19.d.iv.5. + if (lengthA === lim) { + return A; + } + + // Step 19.d.iv.6. + p = e; + + // Steps 19.d.iv.7-8. + var numberOfCaptures = std_Math_max(ToLength(z.length) - 1, 0); + + // Step 19.d.iv.9. + var i = 1; + + // Step 19.d.iv.10. + while (i <= numberOfCaptures) { + // Steps 19.d.iv.10.a-b. + DefineDataProperty(A, lengthA, z[i]); + + // Step 19.d.iv.10.c. + i++; + + // Step 19.d.iv.10.d. + lengthA++; + + // Step 19.d.iv.10.e. + if (lengthA === lim) { + return A; + } + } + + // Step 19.d.iv.11. + q = p; + } + + // Steps 20-22. + if (p >= size) { + DefineDataProperty(A, lengthA, ""); + } else { + DefineDataProperty(A, lengthA, Substring(S, p, size - p)); + } + + // Step 23. + return A; +} + +// ES6 21.2.5.2. +// NOTE: This is not RegExpExec (21.2.5.2.1). +function RegExp_prototype_Exec(string) { + // Steps 1-3. + var R = this; + if (!IsObject(R) || !IsRegExpObject(R)) { + return callFunction( + CallRegExpMethodIfWrapped, + R, + string, + "RegExp_prototype_Exec" + ); + } + + // Steps 4-5. + var S = ToString(string); + + // Step 6. + return RegExpBuiltinExec(R, S); +} + +// ES6 21.2.5.13. +function RegExpTest(string) { + // Steps 1-2. + var R = this; + if (!IsObject(R)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, R === null ? "null" : typeof R); + } + + // Steps 3-4. + var S = ToString(string); + + // Steps 5-6. + return RegExpExecForTest(R, S); +} + +// ES 2016 draft Mar 25, 2016 21.2.4.2. +function $RegExpSpecies() { + // Step 1. + return this; +} +SetCanonicalName($RegExpSpecies, "get [Symbol.species]"); + +function IsRegExpMatchAllOptimizable(rx, C) { + if (!IsRegExpObject(rx)) { + return false; + } + + var RegExpCtor = GetBuiltinConstructor("RegExp"); + if (C !== RegExpCtor) { + return false; + } + + var RegExpProto = RegExpCtor.prototype; + return ( + RegExpPrototypeOptimizable(RegExpProto) && + RegExpInstanceOptimizable(rx, RegExpProto) + ); +} + +// String.prototype.matchAll proposal. +// +// RegExp.prototype [ @@matchAll ] ( string ) +function RegExpMatchAll(string) { + // Step 1. + var rx = this; + + // Step 2. + if (!IsObject(rx)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, rx === null ? "null" : typeof rx); + } + + // Step 3. + var str = ToString(string); + + // Step 4. + var C = SpeciesConstructor(rx, GetBuiltinConstructor("RegExp")); + + var source, flags, matcher, lastIndex; + if (IsRegExpMatchAllOptimizable(rx, C)) { + // Step 5, 9-12. + source = UnsafeGetStringFromReservedSlot(rx, REGEXP_SOURCE_SLOT); + flags = UnsafeGetInt32FromReservedSlot(rx, REGEXP_FLAGS_SLOT); + + // Step 6. + matcher = rx; + + // Step 7. + lastIndex = ToLength(rx.lastIndex); + + // Step 8 (not applicable for the optimized path). + } else { + // Step 5. + source = ""; + flags = ToString(rx.flags); + + // Step 6. + matcher = constructContentFunction(C, C, rx, flags); + + // Steps 7-8. + matcher.lastIndex = ToLength(rx.lastIndex); + + // Steps 9-12. + flags = + (callFunction(std_String_includes, flags, "g") ? REGEXP_GLOBAL_FLAG : 0) | + (callFunction(std_String_includes, flags, "u") ? REGEXP_UNICODE_FLAG : 0); + + // Take the non-optimized path. + lastIndex = REGEXP_STRING_ITERATOR_LASTINDEX_SLOW; + } + + // Step 13. + return CreateRegExpStringIterator(matcher, str, source, flags, lastIndex); +} + +// String.prototype.matchAll proposal. +// +// CreateRegExpStringIterator ( R, S, global, fullUnicode ) +function CreateRegExpStringIterator(regexp, string, source, flags, lastIndex) { + // Step 1. + assert(typeof string === "string", "|string| is a string value"); + + // Steps 2-3. + assert(typeof flags === "number", "|flags| is a number value"); + + assert(typeof source === "string", "|source| is a string value"); + assert(typeof lastIndex === "number", "|lastIndex| is a number value"); + + // Steps 4-9. + var iterator = NewRegExpStringIterator(); + UnsafeSetReservedSlot(iterator, REGEXP_STRING_ITERATOR_REGEXP_SLOT, regexp); + UnsafeSetReservedSlot(iterator, REGEXP_STRING_ITERATOR_STRING_SLOT, string); + UnsafeSetReservedSlot(iterator, REGEXP_STRING_ITERATOR_SOURCE_SLOT, source); + UnsafeSetReservedSlot(iterator, REGEXP_STRING_ITERATOR_FLAGS_SLOT, flags | 0); + UnsafeSetReservedSlot( + iterator, + REGEXP_STRING_ITERATOR_LASTINDEX_SLOT, + lastIndex + ); + + // Step 10. + return iterator; +} + +function IsRegExpStringIteratorNextOptimizable() { + var RegExpProto = GetBuiltinPrototype("RegExp"); + // If RegExpPrototypeOptimizable succeeds, `RegExpProto.exec` is + // guaranteed to be a data property. + return ( + RegExpPrototypeOptimizable(RegExpProto) && + RegExpProto.exec === RegExp_prototype_Exec + ); +} + +// String.prototype.matchAll proposal. +// +// %RegExpStringIteratorPrototype%.next ( ) +function RegExpStringIteratorNext() { + // Steps 1-3. + var obj = this; + if (!IsObject(obj) || (obj = GuardToRegExpStringIterator(obj)) === null) { + return callFunction( + CallRegExpStringIteratorMethodIfWrapped, + this, + "RegExpStringIteratorNext" + ); + } + + var result = { value: undefined, done: false }; + + // Step 4. + var lastIndex = UnsafeGetReservedSlot( + obj, + REGEXP_STRING_ITERATOR_LASTINDEX_SLOT + ); + if (lastIndex === REGEXP_STRING_ITERATOR_LASTINDEX_DONE) { + result.done = true; + return result; + } + + // Step 5. + var regexp = UnsafeGetObjectFromReservedSlot( + obj, + REGEXP_STRING_ITERATOR_REGEXP_SLOT + ); + + // Step 6. + var string = UnsafeGetStringFromReservedSlot( + obj, + REGEXP_STRING_ITERATOR_STRING_SLOT + ); + + // Steps 7-8. + var flags = UnsafeGetInt32FromReservedSlot( + obj, + REGEXP_STRING_ITERATOR_FLAGS_SLOT + ); + var global = !!(flags & REGEXP_GLOBAL_FLAG); + var fullUnicode = !!(flags & REGEXP_UNICODE_FLAG); + + if (lastIndex >= 0) { + assert(IsRegExpObject(regexp), "|regexp| is a RegExp object"); + + var source = UnsafeGetStringFromReservedSlot( + obj, + REGEXP_STRING_ITERATOR_SOURCE_SLOT + ); + if ( + IsRegExpStringIteratorNextOptimizable() && + UnsafeGetStringFromReservedSlot(regexp, REGEXP_SOURCE_SLOT) === source && + UnsafeGetInt32FromReservedSlot(regexp, REGEXP_FLAGS_SLOT) === flags + ) { + // Step 9 (Inlined RegExpBuiltinExec). + var globalOrSticky = !!( + flags & + (REGEXP_GLOBAL_FLAG | REGEXP_STICKY_FLAG) + ); + if (!globalOrSticky) { + lastIndex = 0; + } + + var match = + lastIndex <= string.length + ? RegExpMatcher(regexp, string, lastIndex) + : null; + + // Step 10. + if (match === null) { + // Step 10.a. + UnsafeSetReservedSlot( + obj, + REGEXP_STRING_ITERATOR_LASTINDEX_SLOT, + REGEXP_STRING_ITERATOR_LASTINDEX_DONE + ); + + // Step 10.b. + result.done = true; + return result; + } + + // Step 11.a. + if (global) { + // Step 11.a.i. + var matchLength = match[0].length; + lastIndex = match.index + matchLength; + + // Step 11.a.ii. + if (matchLength === 0) { + // Steps 11.a.ii.1-3. + lastIndex = fullUnicode + ? AdvanceStringIndex(string, lastIndex) + : lastIndex + 1; + } + + UnsafeSetReservedSlot( + obj, + REGEXP_STRING_ITERATOR_LASTINDEX_SLOT, + lastIndex + ); + } else { + // Step 11.b.i. + UnsafeSetReservedSlot( + obj, + REGEXP_STRING_ITERATOR_LASTINDEX_SLOT, + REGEXP_STRING_ITERATOR_LASTINDEX_DONE + ); + } + + // Steps 11.a.iii and 11.b.ii. + result.value = match; + return result; + } + + // Reify the RegExp object. + regexp = RegExpConstructRaw(source, flags); + regexp.lastIndex = lastIndex; + UnsafeSetReservedSlot(obj, REGEXP_STRING_ITERATOR_REGEXP_SLOT, regexp); + + // Mark the iterator as no longer optimizable. + UnsafeSetReservedSlot( + obj, + REGEXP_STRING_ITERATOR_LASTINDEX_SLOT, + REGEXP_STRING_ITERATOR_LASTINDEX_SLOW + ); + } + + // Step 9. + var match = RegExpExec(regexp, string); + + // Step 10. + if (match === null) { + // Step 10.a. + UnsafeSetReservedSlot( + obj, + REGEXP_STRING_ITERATOR_LASTINDEX_SLOT, + REGEXP_STRING_ITERATOR_LASTINDEX_DONE + ); + + // Step 10.b. + result.done = true; + return result; + } + + // Step 11.a. + if (global) { + // Step 11.a.i. + var matchStr = ToString(match[0]); + + // Step 11.a.ii. + if (matchStr.length === 0) { + // Step 11.a.ii.1. + var thisIndex = ToLength(regexp.lastIndex); + + // Step 11.a.ii.2. + var nextIndex = fullUnicode + ? AdvanceStringIndex(string, thisIndex) + : thisIndex + 1; + + // Step 11.a.ii.3. + regexp.lastIndex = nextIndex; + } + } else { + // Step 11.b.i. + UnsafeSetReservedSlot( + obj, + REGEXP_STRING_ITERATOR_LASTINDEX_SLOT, + REGEXP_STRING_ITERATOR_LASTINDEX_DONE + ); + } + + // Steps 11.a.iii and 11.b.ii. + result.value = match; + return result; +} + +// ES2020 draft rev e97c95d064750fb949b6778584702dd658cf5624 +// 7.2.8 IsRegExp ( argument ) +function IsRegExp(argument) { + // Step 1. + if (!IsObject(argument)) { + return false; + } + + // Step 2. + var matcher = argument[GetBuiltinSymbol("match")]; + + // Step 3. + if (matcher !== undefined) { + return !!matcher; + } + + // Steps 4-5. + return IsPossiblyWrappedRegExpObject(argument); +} diff --git a/js/src/builtin/RegExpGlobalReplaceOpt.h.js b/js/src/builtin/RegExpGlobalReplaceOpt.h.js new file mode 100644 index 0000000000..9a88ff9ae6 --- /dev/null +++ b/js/src/builtin/RegExpGlobalReplaceOpt.h.js @@ -0,0 +1,174 @@ +/* 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/. */ + +// Function template for the following functions: +// * RegExpGlobalReplaceOpt +// * RegExpGlobalReplaceOptFunc +// * RegExpGlobalReplaceOptSubst +// * RegExpGlobalReplaceOptElemBase +// Define the following macro and include this file to declare function: +// * FUNC_NAME -- function name (required) +// e.g. +// #define FUNC_NAME RegExpGlobalReplaceOpt +// Define the following macro (without value) to switch the code: +// * SUBSTITUTION -- replaceValue is a string with "$" +// * FUNCTIONAL -- replaceValue is a function +// * ELEMBASE -- replaceValue is a function that returns an element +// of an object +// * none of above -- replaceValue is a string without "$" + +// ES2023 draft rev 2c78e6f6b5bc6bfbf79dd8a12a9593e5b57afcd2 +// 22.2.5.11 RegExp.prototype [ @@replace ] ( string, replaceValue ) +// steps 9-17. +// Optimized path for @@replace with the following conditions: +// * global flag is true +function FUNC_NAME( + rx, + S, + lengthS, + replaceValue, + flags, +#ifdef SUBSTITUTION + firstDollarIndex, +#endif +#ifdef ELEMBASE + elemBase +#endif +) { + // Step 9.a. + var fullUnicode = !!(flags & REGEXP_UNICODE_FLAG); + + // Step 9.b. + var lastIndex = 0; + rx.lastIndex = 0; + +#if defined(FUNCTIONAL) || defined(ELEMBASE) + // Save the original source and flags, so we can check if the replacer + // function recompiled the regexp. + var originalSource = UnsafeGetStringFromReservedSlot(rx, REGEXP_SOURCE_SLOT); + var originalFlags = flags; +#endif + + // Step 13 (reordered). + var accumulatedResult = ""; + + // Step 14 (reordered). + var nextSourcePosition = 0; + + // Step 12. + while (true) { + // Step 12.a. + var result = RegExpMatcher(rx, S, lastIndex); + + // Step 12.b. + if (result === null) { + break; + } + + // Steps 15.a-b (skipped). + assert(result.length >= 1, "RegExpMatcher doesn't return an empty array"); + + // Step 15.c. + var matched = result[0]; + + // Step 15.d. + var matchLength = matched.length | 0; + + // Steps 15.e-f. + var position = result.index | 0; + lastIndex = position + matchLength; + + // Steps 15.g-l. + var replacement; +#if defined(FUNCTIONAL) + replacement = RegExpGetFunctionalReplacement( + result, + S, + position, + replaceValue + ); +#elif defined(SUBSTITUTION) + // Step 15.l.i + var namedCaptures = result.groups; + if (namedCaptures !== undefined) { + namedCaptures = ToObject(namedCaptures); + } + // Step 15.l.ii + replacement = RegExpGetSubstitution( + result, + S, + position, + replaceValue, + firstDollarIndex, + namedCaptures + ); +#elif defined(ELEMBASE) + if (IsObject(elemBase)) { + var prop = GetStringDataProperty(elemBase, matched); + if (prop !== undefined) { + assert( + typeof prop === "string", + "GetStringDataProperty should return either string or undefined" + ); + replacement = prop; + } else { + elemBase = undefined; + } + } + + if (!IsObject(elemBase)) { + replacement = RegExpGetFunctionalReplacement( + result, + S, + position, + replaceValue + ); + } +#else + replacement = replaceValue; +#endif + + // Step 15.m.ii. + accumulatedResult += + Substring(S, nextSourcePosition, position - nextSourcePosition) + + replacement; + + // Step 15.m.iii. + nextSourcePosition = lastIndex; + + // Step 12.c.iii.2. + if (matchLength === 0) { + lastIndex = fullUnicode + ? AdvanceStringIndex(S, lastIndex) + : lastIndex + 1; + if (lastIndex > lengthS) { + break; + } + lastIndex |= 0; + } + +#if defined(FUNCTIONAL) || defined(ELEMBASE) + // Ensure the current source and flags match the original regexp, the + // replaceValue function may have called RegExp#compile. + if ( + UnsafeGetStringFromReservedSlot(rx, REGEXP_SOURCE_SLOT) !== + originalSource || + UnsafeGetInt32FromReservedSlot(rx, REGEXP_FLAGS_SLOT) !== originalFlags + ) { + rx = RegExpConstructRaw(originalSource, originalFlags); + } +#endif + } + + // Step 16. + if (nextSourcePosition >= lengthS) { + return accumulatedResult; + } + + // Step 17. + return ( + accumulatedResult + + Substring(S, nextSourcePosition, lengthS - nextSourcePosition) + ); +} diff --git a/js/src/builtin/RegExpLocalReplaceOpt.h.js b/js/src/builtin/RegExpLocalReplaceOpt.h.js new file mode 100644 index 0000000000..35bb8a3bd9 --- /dev/null +++ b/js/src/builtin/RegExpLocalReplaceOpt.h.js @@ -0,0 +1,164 @@ +/* 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/. */ + +// Function template for the following functions: +// * RegExpLocalReplaceOpt +// * RegExpLocalReplaceOptFunc +// * RegExpLocalReplaceOptSubst +// Define the following macro and include this file to declare function: +// * FUNC_NAME -- function name (required) +// e.g. +// #define FUNC_NAME RegExpLocalReplaceOpt +// Define the following macro (without value) to switch the code: +// * SUBSTITUTION -- replaceValue is a string with "$" +// * FUNCTIONAL -- replaceValue is a function +// * SHORT_STRING -- replaceValue is a string without "$" and lengthS < 0x7fff +// * neither of above -- replaceValue is a string without "$" + +// ES2023 draft rev 2c78e6f6b5bc6bfbf79dd8a12a9593e5b57afcd2 +// 22.2.5.11 RegExp.prototype [ @@replace ] ( string, replaceValue ) +// Steps 12.a-17. +// Optimized path for @@replace with the following conditions: +// * global flag is false +function FUNC_NAME( + rx, + S, + lengthS, + replaceValue, +#ifdef SUBSTITUTION + firstDollarIndex +#endif +) { + // 21.2.5.2.2 RegExpBuiltinExec, step 4. + var lastIndex = ToLength(rx.lastIndex); + + // 21.2.5.2.2 RegExpBuiltinExec, step 5. + // Side-effects in step 4 can recompile the RegExp, so we need to read the + // flags again and handle the case when global was enabled even though this + // function is optimized for non-global RegExps. + var flags = UnsafeGetInt32FromReservedSlot(rx, REGEXP_FLAGS_SLOT); + + // 21.2.5.2.2 RegExpBuiltinExec, steps 6-7. + var globalOrSticky = !!(flags & (REGEXP_GLOBAL_FLAG | REGEXP_STICKY_FLAG)); + + if (globalOrSticky) { + // 21.2.5.2.2 RegExpBuiltinExec, step 12.a. + if (lastIndex > lengthS) { + if (globalOrSticky) { + rx.lastIndex = 0; + } + + // Steps 12-16. + return S; + } + } else { + // 21.2.5.2.2 RegExpBuiltinExec, step 8. + lastIndex = 0; + } + +#if !defined(SHORT_STRING) + // Step 12.a. + var result = RegExpMatcher(rx, S, lastIndex); + + // Step 12.b. + if (result === null) { + // 21.2.5.2.2 RegExpBuiltinExec, steps 12.a.i, 12.c.i. + if (globalOrSticky) { + rx.lastIndex = 0; + } + + // Steps 13-17. + return S; + } +#else + // Step 12.a. + var result = RegExpSearcher(rx, S, lastIndex); + + // Step 12.b. + if (result === -1) { + // 21.2.5.2.2 RegExpBuiltinExec, steps 12.a.i, 12.c.i. + if (globalOrSticky) { + rx.lastIndex = 0; + } + + // Steps 13-17. + return S; + } +#endif + + // Steps 12.c, 13-14. + +#if !defined(SHORT_STRING) + // Steps 15.a-b. + assert(result.length >= 1, "RegExpMatcher doesn't return an empty array"); + + // Step 15.c. + var matched = result[0]; + + // Step 15.d. + var matchLength = matched.length; + + // Step 15.e-f. + var position = result.index; + + // Step 15.m.iii (reordered) + // To set rx.lastIndex before RegExpGetFunctionalReplacement. + var nextSourcePosition = position + matchLength; +#else + // Steps 15.a-d (skipped). + + // Step 15.e-f. + var position = result & 0x7fff; + + // Step 15.m.iii (reordered) + var nextSourcePosition = (result >> 15) & 0x7fff; +#endif + + // 21.2.5.2.2 RegExpBuiltinExec, step 15. + if (globalOrSticky) { + rx.lastIndex = nextSourcePosition; + } + + var replacement; + // Steps 15.g-l. +#if defined(FUNCTIONAL) + replacement = RegExpGetFunctionalReplacement( + result, + S, + position, + replaceValue + ); +#elif defined(SUBSTITUTION) + // Step 15.l.i + var namedCaptures = result.groups; + if (namedCaptures !== undefined) { + namedCaptures = ToObject(namedCaptures); + } + // Step 15.l.ii + replacement = RegExpGetSubstitution( + result, + S, + position, + replaceValue, + firstDollarIndex, + namedCaptures + ); +#else + replacement = replaceValue; +#endif + + // Step 15.m.ii. + var accumulatedResult = Substring(S, 0, position) + replacement; + + // Step 16. + if (nextSourcePosition >= lengthS) { + return accumulatedResult; + } + + // Step 17. + return ( + accumulatedResult + + Substring(S, nextSourcePosition, lengthS - nextSourcePosition) + ); +} diff --git a/js/src/builtin/SelfHostingDefines.h b/js/src/builtin/SelfHostingDefines.h new file mode 100644 index 0000000000..8a589676bf --- /dev/null +++ b/js/src/builtin/SelfHostingDefines.h @@ -0,0 +1,125 @@ +/* -*- 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/. */ + +// Specialized .h file to be used by both JS and C++ code. + +#ifndef builtin_SelfHostingDefines_h +#define builtin_SelfHostingDefines_h + +// Utility macros. +#define TO_INT32(x) ((x) | 0) +#define TO_UINT32(x) ((x) >>> 0) +#define IS_UINT32(x) ((x) >>> 0 == = (x)) +#define MAX_UINT32 0xffffffff +#define MAX_NUMERIC_INDEX 0x1fffffffffffff // == Math.pow(2, 53) - 1 + +// Unforgeable version of Function.prototype.apply. +#define FUN_APPLY(FUN, RECEIVER, ARGS) \ + callFunction(std_Function_apply, FUN, RECEIVER, ARGS) + +// NB: keep this in sync with the copy in vm/ArgumentsObject.h. +#define MAX_ARGS_LENGTH (500 * 1000) + +// NB: keep this in sync with JS::MaxStringLength in js/public/String.h. +#define MAX_STRING_LENGTH ((1 << 30) - 2) + +// Spread non-empty argument list of up to 15 elements. +#define SPREAD(v, n) SPREAD_##n(v) +#define SPREAD_1(v) v[0] +#define SPREAD_2(v) SPREAD_1(v), v[1] +#define SPREAD_3(v) SPREAD_2(v), v[2] +#define SPREAD_4(v) SPREAD_3(v), v[3] +#define SPREAD_5(v) SPREAD_4(v), v[4] +#define SPREAD_6(v) SPREAD_5(v), v[5] +#define SPREAD_7(v) SPREAD_6(v), v[6] +#define SPREAD_8(v) SPREAD_7(v), v[7] +#define SPREAD_9(v) SPREAD_8(v), v[8] +#define SPREAD_10(v) SPREAD_9(v), v[9] +#define SPREAD_11(v) SPREAD_10(v), v[10] +#define SPREAD_12(v) SPREAD_11(v), v[11] +#define SPREAD_13(v) SPREAD_12(v), v[12] +#define SPREAD_14(v) SPREAD_13(v), v[13] +#define SPREAD_15(v) SPREAD_14(v), v[14] + +// Property descriptor attributes. +#define ATTR_ENUMERABLE 0x01 +#define ATTR_CONFIGURABLE 0x02 +#define ATTR_WRITABLE 0x04 + +#define ATTR_NONENUMERABLE 0x08 +#define ATTR_NONCONFIGURABLE 0x10 +#define ATTR_NONWRITABLE 0x20 + +// Property descriptor kind, must be different from the descriptor attributes. +#define DATA_DESCRIPTOR_KIND 0x100 +#define ACCESSOR_DESCRIPTOR_KIND 0x200 + +// Property descriptor array indices. +#define PROP_DESC_ATTRS_AND_KIND_INDEX 0 +#define PROP_DESC_VALUE_INDEX 1 +#define PROP_DESC_GETTER_INDEX 1 +#define PROP_DESC_SETTER_INDEX 2 + +// The extended slot of cloned self-hosted function, in which the self-hosted +// name for self-hosted builtins is stored. +#define LAZY_FUNCTION_NAME_SLOT 0 + +#define ITERATOR_SLOT_TARGET 0 +// Used for collection iterators. +#define ITERATOR_SLOT_RANGE 1 +// Used for list, i.e. Array and String, iterators. +#define ITERATOR_SLOT_NEXT_INDEX 1 +#define ITERATOR_SLOT_ITEM_KIND 2 + +#define ITEM_KIND_KEY 0 +#define ITEM_KIND_VALUE 1 +#define ITEM_KIND_KEY_AND_VALUE 2 + +#define REGEXP_SOURCE_SLOT 1 +#define REGEXP_FLAGS_SLOT 2 + +#define REGEXP_IGNORECASE_FLAG 0x01 +#define REGEXP_GLOBAL_FLAG 0x02 +#define REGEXP_MULTILINE_FLAG 0x04 +#define REGEXP_STICKY_FLAG 0x08 +#define REGEXP_UNICODE_FLAG 0x10 +#define REGEXP_DOTALL_FLAG 0x20 +#define REGEXP_HASINDICES_FLAG 0x40 + +#define REGEXP_STRING_ITERATOR_REGEXP_SLOT 0 +#define REGEXP_STRING_ITERATOR_STRING_SLOT 1 +#define REGEXP_STRING_ITERATOR_SOURCE_SLOT 2 +#define REGEXP_STRING_ITERATOR_FLAGS_SLOT 3 +#define REGEXP_STRING_ITERATOR_LASTINDEX_SLOT 4 + +#define REGEXP_STRING_ITERATOR_LASTINDEX_DONE -1 +#define REGEXP_STRING_ITERATOR_LASTINDEX_SLOW -2 + +#define DATE_METHOD_LOCALE_TIME_STRING 0 +#define DATE_METHOD_LOCALE_DATE_STRING 1 +#define DATE_METHOD_LOCALE_STRING 2 + +#define INTL_INTERNALS_OBJECT_SLOT 0 + +#define TYPEDARRAY_KIND_INT8 0 +#define TYPEDARRAY_KIND_UINT8 1 +#define TYPEDARRAY_KIND_INT16 2 +#define TYPEDARRAY_KIND_UINT16 3 +#define TYPEDARRAY_KIND_INT32 4 +#define TYPEDARRAY_KIND_UINT32 5 +#define TYPEDARRAY_KIND_FLOAT32 6 +#define TYPEDARRAY_KIND_FLOAT64 7 +#define TYPEDARRAY_KIND_UINT8CLAMPED 8 +#define TYPEDARRAY_KIND_BIGINT64 9 +#define TYPEDARRAY_KIND_BIGUINT64 10 + +#define ITERATED_SLOT 0 + +#define ITERATOR_HELPER_GENERATOR_SLOT 0 + +#define ASYNC_ITERATOR_HELPER_GENERATOR_SLOT 0 + +#endif diff --git a/js/src/builtin/Set.js b/js/src/builtin/Set.js new file mode 100644 index 0000000000..ed52a2c7fc --- /dev/null +++ b/js/src/builtin/Set.js @@ -0,0 +1,597 @@ +/* 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/. */ + +// ES2017 draft rev 0e10c9f29fca1385980c08a7d5e7bb3eb775e2e4 +// 23.2.1.1 Set, steps 6-8 +function SetConstructorInit(iterable) { + var set = this; + + // Step 6.a. + var adder = set.add; + + // Step 6.b. + if (!IsCallable(adder)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, typeof adder); + } + + // Steps 6.c-8. + for (var nextValue of allowContentIter(iterable)) { + callContentFunction(adder, set, nextValue); + } +} + +#ifdef ENABLE_NEW_SET_METHODS +// New Set methods proposal +// +// Set.prototype.union(iterable) +// https://tc39.es/proposal-set-methods/#Set.prototype.union +function SetUnion(iterable) { + // Step 1. Let set be the this value. + var set = this; + + // Step 2. If Type(set) is not Object, throw a TypeError exception. + if (!IsObject(set)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, set === null ? "null" : typeof set); + } + + // Step 3. Let Ctr be ? SpeciesConstructor(set, %Set%). + var Ctr = SpeciesConstructor(set, GetBuiltinConstructor("Set")); + + // Step 4. Let newSet be ? Construct(Ctr, set). + var newSet = constructContentFunction(Ctr, Ctr, set); + + // Step 5. Let adder be ? Get(newSet, "add"). + var adder = newSet.add; + + // Inlined AddEntryFromIterable Step 1. If IsCallable(adder) is false, + // throw a TypeError exception. + if (!IsCallable(adder)) { + ThrowTypeError(JSMSG_PROPERTY_NOT_CALLABLE, "add"); + } + + // Step 6. Return ? AddEntryFromIterable(newSet, iterable, adder). + return AddEntryFromIterable(newSet, iterable, adder); +} + +// New Set methods proposal +// +// Set.prototype.intersection(iterable) +// https://tc39.es/proposal-set-methods/#Set.prototype.intersection +function SetIntersection(iterable) { + // Step 1. Let set be the this value. + var set = this; + + // Step 2. If Type(set) is not Object, throw a TypeError exception. + if (!IsObject(set)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, set === null ? "null" : typeof set); + } + + // Step 3. Let Ctr be ? SpeciesConstructor(set, %Set%). + var Ctr = SpeciesConstructor(set, GetBuiltinConstructor("Set")); + + // Step 4. Let newSet be ? Construct(Ctr). + var newSet = constructContentFunction(Ctr, Ctr); + + // Step 5. Let hasCheck be ? Get(set, "has"). + var hasCheck = set.has; + + // Step 6. If IsCallable(hasCheck) is false, throw a TypeError exception. + if (!IsCallable(hasCheck)) { + ThrowTypeError(JSMSG_PROPERTY_NOT_CALLABLE, "has"); + } + + // Step 7. Let adder be ? Get(newSet, "add"). + var adder = newSet.add; + + // Step 8. If IsCallable(adder) is false, throw a TypeError exception. + if (!IsCallable(adder)) { + ThrowTypeError(JSMSG_PROPERTY_NOT_CALLABLE, "add"); + } + + // Step 9. Let iteratorRecord be ? GetIterator(iterable). + var iteratorRecord = GetIteratorSync(iterable); + + // Step 10. Repeat, + while (true) { + // Step a. Let next be ? IteratorStep(iteratorRecord). + var next = IteratorStep(iteratorRecord); + + // Step b. If next is false, return newSet. + if (!next) { + return newSet; + } + + // Step c. Let nextValue be ? IteratorValue(next). + var nextValue = next.value; + var needClose = true; + var has; + try { + // Step d. Let has be Call(hasCheck, set, « nextValue »). + has = callContentFunction(hasCheck, set, nextValue); + needClose = false; + } finally { + if (needClose) { + // Step e. If has is an abrupt completion, + // return ? IteratorClose(iteratorRecord, has). + IteratorClose(iteratorRecord); + } + } + + // Step f. If has.[[Value]] is true, + if (has) { + needClose = true; + try { + // Step i. Let status be Call(adder, newSet, « nextValue »). + callContentFunction(adder, newSet, nextValue); + needClose = false; + } finally { + if (needClose) { + // Step ii. If status is an abrupt completion, return ? + // IteratorClose(iteratorRecord, status). + IteratorClose(iteratorRecord); + } + } + } + } +} + +// New Set methods proposal +// +// Set.prototype.difference(iterable) +// https://tc39.es/proposal-set-methods/#Set.prototype.difference +function SetDifference(iterable) { + // Step 1. Let set be the this value. + var set = this; + + // Step 2. If Type(set) is not Object, throw a TypeError exception. + if (!IsObject(set)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, set === null ? "null" : typeof set); + } + + // Step 3. Let Ctr be ? SpeciesConstructor(set, %Set%). + var Ctr = SpeciesConstructor(set, GetBuiltinConstructor("Set")); + + // Step 4. Let newSet be ? Construct(Ctr, set). + var newSet = constructContentFunction(Ctr, Ctr, set); + + // Step 5. Let remover be ? Get(newSet, "delete"). + var remover = newSet.delete; + + // Step 6. If IsCallable(remover) is false, throw a TypeError exception. + if (!IsCallable(remover)) { + ThrowTypeError(JSMSG_PROPERTY_NOT_CALLABLE, "delete"); + } + + // Step 7. Let iteratorRecord be ? GetIterator(iterable). + var iteratorRecord = GetIteratorSync(iterable); + + // Step 8. Repeat, + while (true) { + // Step a. Let next be ? IteratorStep(iteratorRecord). + var next = IteratorStep(iteratorRecord); + + // Step b. If next is false, return newSet. + if (!next) { + return newSet; + } + + // Step c. Let nextValue be ? IteratorValue(next). + var nextValue = next.value; + var needClose = true; + try { + // Step d. Let status be Call(remover, newSet, « nextValue »). + callContentFunction(remover, newSet, nextValue); + needClose = false; + } finally { + if (needClose) { + // Step e. If status is an abrupt completion, + // return ? IteratorClose(iteratorRecord, status). + IteratorClose(iteratorRecord); + } + } + } +} + +// New Set methods proposal +// +// Set.prototype.symmetricDifference(iterable) +// https://tc39.es/proposal-set-methods/#Set.prototype.symmetricDifference +function SetSymmetricDifference(iterable) { + // Step 1. Let set be the this value. + var set = this; + + // Step 2. If Type(set) is not Object, throw a TypeError exception. + if (!IsObject(set)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, set === null ? "null" : typeof set); + } + + // Step 3. Let Ctr be ? SpeciesConstructor(set, %Set%). + var Ctr = SpeciesConstructor(set, GetBuiltinConstructor("Set")); + + // Step 4. Let newSet be ? Construct(Ctr, set). + var newSet = constructContentFunction(Ctr, Ctr, set); + + // Step 5. Let remover be ? Get(newSet, "delete"). + var remover = newSet.delete; + + // Step 6. If IsCallable(remover) is false, throw a TypeError exception. + if (!IsCallable(remover)) { + ThrowTypeError(JSMSG_PROPERTY_NOT_CALLABLE, "delete"); + } + + // Step 7. Let adder be ? Get(newSet, "add"). + var adder = newSet.add; + + // Step 8. If IsCallable(adder) is false, throw a TypeError exception. + if (!IsCallable(adder)) { + ThrowTypeError(JSMSG_PROPERTY_NOT_CALLABLE, "add"); + } + + // Step 9. Let iteratorRecord be ? GetIterator(iterable). + var iteratorRecord = GetIteratorSync(iterable); + + // Step 10. Repeat, + while (true) { + // Step a. Let next be ? IteratorStep(iteratorRecord). + var next = IteratorStep(iteratorRecord); + + // Step b. If next is false, return newSet. + if (!next) { + return newSet; + } + + // Step c. Let nextValue be ? IteratorValue(next). + var nextValue = next.value; + var needClose = true; + var removed; + try { + // Step d. Let removed be Call(remover, newSet, « nextValue »). + removed = callContentFunction(remover, newSet, nextValue); + needClose = false; + } finally { + if (needClose) { + // Step e. If removed is an abrupt completion, + // return ? IteratorClose(iteratorRecord, removed). + IteratorClose(iteratorRecord); + } + } + + // Step f. If removed.[[Value]] is false, + if (!removed) { + needClose = true; + try { + // Step i. Let status be Call(adder, newSet, « nextValue »). + callContentFunction(adder, newSet, nextValue); + needClose = false; + } finally { + if (needClose) { + // Step ii. If status is an abrupt completion, + // return ? IteratorClose(iteratorRecord, status). + IteratorClose(iteratorRecord); + } + } + } + } +} + +// New Set methods proposal +// +// Set.prototype.isSubsetOf(iterable) +// https://tc39.es/proposal-set-methods/#Set.prototype.isSubsetOf +function SetIsSubsetOf(iterable) { + // Step 1. Let set be the this value. + var set = this; + + // Step 2. Let iteratorRecord be ? GetIterator(set). + var iteratorRecord = GetIteratorSync(set); + + // Step 3. If Type(iterable) is not Object, throw a TypeError exception. + if (!IsObject(iterable)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, set === null ? "null" : typeof set); + } + + // Step 4. Let otherSet be iterable. + var otherSet = iterable; + + // Step 5. Let hasCheck be ? Get(otherSet, "has"). + var hasCheck = otherSet.has; + + // Step 6. If IsCallable(hasCheck) is false, + if (!IsCallable(hasCheck)) { + // Step a. Let otherSet be ? Construct(%Set%). + let set = GetBuiltinConstructor("Set"); + otherSet = new set(); + + // We are not inlining AddEntryFromIterable Step 1 here because we know + // std_Set_add is callable Step b. Perform ? + // AddEntryFromIterable(otherSet, iterable, %SetProto_add%). + AddEntryFromIterable(otherSet, iterable, std_Set_add); + + // Step c. Let hasCheck be %SetProto_has%. + hasCheck = std_Set_has; + } + + // Step 7. Repeat, + while (true) { + // Step a. Let next be ? IteratorStep(iteratorRecord). + var next = IteratorStep(iteratorRecord); + + // Step b. If next is false, return true. + if (!next) { + return true; + } + + // Step c. Let nextValue be ? IteratorValue(next). + var nextValue = next.value; + var needClose = true; + var has; + try { + // Step d. Let has be Call(hasCheck, otherSet, « nextValue »). + has = callContentFunction(hasCheck, otherSet, nextValue); + needClose = false; + } finally { + if (needClose) { + // Step e. If has is an abrupt completion, + // return ? IteratorClose(iteratorRecord, has). + IteratorClose(iteratorRecord); + } + } + + // Step f. If has.[[Value]] is false, return false. + if (!has) { + return false; + } + } +} + +// New Set methods proposal +// +// Set.prototype.isSupersetOf(iterable) +// https://tc39.es/proposal-set-methods/#Set.prototype.isSupersetOf +function SetIsSupersetOf(iterable) { + // Step 1. Let set be the this value. + var set = this; + + // Step 2. If Type(set) is not Object, throw a TypeError exception. + if (!IsObject(set)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, set === null ? "null" : typeof set); + } + + // Step 3. Let hasCheck be ? Get(set, "has"). + var hasCheck = set.has; + + // Step 4. If IsCallable(hasCheck) is false, throw a TypeError exception. + if (!IsCallable(hasCheck)) { + ThrowTypeError(JSMSG_PROPERTY_NOT_CALLABLE, "has"); + } + + // Step 5. Let iteratorRecord be ? GetIterator(iterable). + var iteratorRecord = GetIteratorSync(iterable); + + // Step 6. Repeat, + while (true) { + // Step a. Let next be ? IteratorStep(iteratorRecord). + var next = IteratorStep(iteratorRecord); + + // Step b. If next is false, return true. + if (!next) { + return true; + } + + // Step c. Let nextValue be ? IteratorValue(next). + var nextValue = next.value; + var needClose = true; + var has; + try { + // Step d. Let has be Call(hasCheck, set, « nextValue »). + has = callContentFunction(hasCheck, set, nextValue); + needClose = false; + } finally { + if (needClose) { + // Step e. If has is an abrupt completion, + // return ? IteratorClose(iteratorRecord, has). + IteratorClose(iteratorRecord); + } + } + + // Step f. If has.[[Value]] is false, return false. + if (!has) { + return false; + } + } +} + +// New Set methods proposal +// +// Set.prototype.isDisjointFrom(iterable) +// https://tc39.es/proposal-set-methods/#Set.prototype.isDisjointFrom +function SetIsDisjointFrom(iterable) { + // Step 1. Let set be the this value. + var set = this; + + // Step 2. If Type(set) is not Object, throw a TypeError exception. + if (!IsObject(set)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, set === null ? "null" : typeof set); + } + + // Step 3. Let hasCheck be ? Get(set, "has"). + var hasCheck = set.has; + + // Step 4. If IsCallable(hasCheck) is false, throw a TypeError exception. + if (!IsCallable(hasCheck)) { + ThrowTypeError(JSMSG_PROPERTY_NOT_CALLABLE, "has"); + } + + // Step 5. Let iteratorRecord be ? GetIterator(iterable). + var iteratorRecord = GetIteratorSync(iterable); + + // Step 6. Repeat, + while (true) { + // Step a. Let next be ? IteratorStep(iteratorRecord). + var next = IteratorStep(iteratorRecord); + + // Step b. If next is false, return true. + if (!next) { + return true; + } + + // Step c. Let nextValue be ? IteratorValue(next). + var nextValue = next.value; + var needClose = true; + var has; + try { + // Step d. Let has be Call(hasCheck, set, « nextValue »). + has = callContentFunction(hasCheck, set, nextValue); + needClose = false; + } finally { + if (needClose) { + // Step e. If has is an abrupt completion, + // return ? IteratorClose(iteratorRecord, has). + IteratorClose(iteratorRecord); + } + } + + // Step f. If has.[[Value]] is true, return false. + if (has) { + return false; + } + } +} + +// New Set methods proposal +// +// AddEntryFromIterable ( target, iterable, adder ) +// https://tc39.es/proposal-set-methods/#AddEntryFromIterable +function AddEntryFromIterable(target, iterable, adder) { + assert(IsCallable(adder), "adder argument is callable"); + + // Step 2. Let iteratorRecord be ? GetIterator(iterable). + var iteratorRecord = GetIteratorSync(iterable); + + // Step 3. Repeat, + while (true) { + // Step a. Let next be ? IteratorStep(iteratorRecord). + var next = IteratorStep(iteratorRecord); + + // Step b. If next is false, return target. + if (!next) { + return target; + } + + // Step c. Let nextValue be ? IteratorValue(next). + var nextValue = next.value; + var needClose = true; + try { + // Step d. Let status be Call(adder, target, « nextValue »). + callContentFunction(adder, target, nextValue); + needClose = false; + } finally { + if (needClose) { + // Step e. If status is an abrupt completion, + // return ? IteratorClose(iteratorRecord, status). + IteratorClose(iteratorRecord); + } + } + } +} +#endif + +// ES2018 draft rev f83aa38282c2a60c6916ebc410bfdf105a0f6a54 +// 23.2.3.6 Set.prototype.forEach ( callbackfn [ , thisArg ] ) +function SetForEach(callbackfn, thisArg = undefined) { + // Step 1. + var S = this; + + // Steps 2-3. + if (!IsObject(S) || (S = GuardToSetObject(S)) === null) { + return callFunction( + CallSetMethodIfWrapped, + this, + callbackfn, + thisArg, + "SetForEach" + ); + } + + // Step 4. + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + // Steps 5-8. + var values = callFunction(std_Set_values, S); + + // Inlined: SetIteratorNext + var setIterationResult = globalSetIterationResult; + + while (true) { + var done = GetNextSetEntryForIterator(values, setIterationResult); + if (done) { + break; + } + + var value = setIterationResult[0]; + setIterationResult[0] = null; + + callContentFunction(callbackfn, thisArg, value, value, S); + } +} + +// ES6 final draft 23.2.2.2. +// Uncloned functions with `$` prefix are allocated as extended function +// to store the original name in `SetCanonicalName`. +function $SetSpecies() { + // Step 1. + return this; +} +SetCanonicalName($SetSpecies, "get [Symbol.species]"); + +var globalSetIterationResult = CreateSetIterationResult(); + +function SetIteratorNext() { + // Step 1. + var O = this; + + // Steps 2-3. + if (!IsObject(O) || (O = GuardToSetIterator(O)) === null) { + return callFunction( + CallSetIteratorMethodIfWrapped, + this, + "SetIteratorNext" + ); + } + + // Steps 4-5 (implemented in GetNextSetEntryForIterator). + // Steps 8-9 (omitted). + + var setIterationResult = globalSetIterationResult; + + var retVal = { value: undefined, done: true }; + + // Steps 10.a, 11. + var done = GetNextSetEntryForIterator(O, setIterationResult); + if (!done) { + // Steps 10.b-c (omitted). + + // Step 6. + var itemKind = UnsafeGetInt32FromReservedSlot(O, ITERATOR_SLOT_ITEM_KIND); + + var result; + if (itemKind === ITEM_KIND_VALUE) { + // Step 10.d.i. + result = setIterationResult[0]; + } else { + // Step 10.d.ii. + assert(itemKind === ITEM_KIND_KEY_AND_VALUE, itemKind); + result = [setIterationResult[0], setIterationResult[0]]; + } + + setIterationResult[0] = null; + retVal.value = result; + retVal.done = false; + } + + // Steps 7, 10.d, 12. + return retVal; +} diff --git a/js/src/builtin/ShadowRealm.cpp b/js/src/builtin/ShadowRealm.cpp new file mode 100644 index 0000000000..adc7f3c245 --- /dev/null +++ b/js/src/builtin/ShadowRealm.cpp @@ -0,0 +1,688 @@ +/* -*- 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 "builtin/ShadowRealm.h" + +#include "mozilla/Assertions.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "builtin/ModuleObject.h" +#include "builtin/Promise.h" +#include "builtin/WrappedFunctionObject.h" +#include "frontend/BytecodeCompilation.h" +#include "js/ErrorReport.h" +#include "js/Exception.h" +#include "js/GlobalObject.h" +#include "js/Principals.h" +#include "js/Promise.h" +#include "js/PropertyAndElement.h" +#include "js/PropertyDescriptor.h" +#include "js/ShadowRealmCallbacks.h" +#include "js/SourceText.h" +#include "js/StableStringChars.h" +#include "js/StructuredClone.h" +#include "js/TypeDecls.h" +#include "js/Wrapper.h" +#include "vm/GlobalObject.h" +#include "vm/Interpreter.h" +#include "vm/JSObject.h" +#include "vm/ObjectOperations.h" + +#include "builtin/HandlerFunction-inl.h" +#include "vm/Compartment-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/Realm-inl.h" + +using namespace js; + +using JS::AutoStableStringChars; +using JS::CompileOptions; +using JS::SourceOwnership; +using JS::SourceText; + +static JSObject* DefaultNewShadowRealmGlobal(JSContext* cx, + JS::RealmOptions& options, + JSPrincipals* principals, + Handle<JSObject*> unused) { + static const JSClass shadowRealmGlobal = { + "ShadowRealmGlobal", JSCLASS_GLOBAL_FLAGS, &JS::DefaultGlobalClassOps}; + + return JS_NewGlobalObject(cx, &shadowRealmGlobal, principals, + JS::FireOnNewGlobalHook, options); +} + +// https://tc39.es/proposal-shadowrealm/#sec-shadowrealm-constructor +/*static*/ +bool ShadowRealmObject::construct(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. If NewTarget is undefined, throw a TypeError exception. + if (!args.isConstructing()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_CONSTRUCTOR, "ShadowRealm"); + return false; + } + + // Step 2. Let O be ? OrdinaryCreateFromConstructor(NewTarget, + // "%ShadowRealm.prototype%", « [[ShadowRealm]], [[ExecutionContext]] »). + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_ShadowRealm, + &proto)) { + return false; + } + + Rooted<ShadowRealmObject*> shadowRealmObj( + cx, NewObjectWithClassProto<ShadowRealmObject>(cx, proto)); + if (!shadowRealmObj) { + return false; + } + + // Instead of managing Realms, spidermonkey associates a realm with a global + // object, and so we will manage and store a global. + + // Step 3. Let realmRec be CreateRealm(). + + // Initially steal creation options from current realm: + JS::RealmOptions options(cx->realm()->creationOptions(), + cx->realm()->behaviors()); + + // We don't want to have to deal with CCWs in addition to + // WrappedFunctionObjects. + options.creationOptions().setExistingCompartment(cx->compartment()); + + JS::GlobalCreationCallback newGlobal = + cx->runtime()->getShadowRealmGlobalCreationCallback(); + // If an embedding didn't provide a callback to initialize the global, + // use the basic default one. + if (!newGlobal) { + newGlobal = DefaultNewShadowRealmGlobal; + } + + // Our shadow realm inherits the principals of the current realm, + // but is otherwise constrained. + JSPrincipals* principals = JS::GetRealmPrincipals(cx->realm()); + + // Steps 5-11: In SpiderMonkey these fall under the aegis of the global + // creation. It's worth noting that the newGlobal callback + // needs to respect the SetRealmGlobalObject call below, which + // sets the global to + // OrdinaryObjectCreate(intrinsics.[[%Object.prototype%]]). + // + // Step 5. Let context be a new execution context. + // Step 6. Set the Function of context to null. + // Step 7. Set the Realm of context to realmRec. + // Step 8. Set the ScriptOrModule of context to null. + // Step 9. Set O.[[ExecutionContext]] to context. + // Step 10. Perform ? SetRealmGlobalObject(realmRec, undefined, undefined). + // Step 11. Perform ? SetDefaultGlobalBindings(O.[[ShadowRealm]]). + Rooted<JSObject*> global(cx, + newGlobal(cx, options, principals, cx->global())); + if (!global) { + return false; + } + + // Make sure the new global hook obeyed our request in the + // creation options to have a same compartment global. + MOZ_RELEASE_ASSERT(global->compartment() == cx->compartment()); + + // Step 4. Set O.[[ShadowRealm]] to realmRec. + shadowRealmObj->initFixedSlot(GlobalSlot, ObjectValue(*global)); + + // Step 12. Perform ? HostInitializeShadowRealm(O.[[ShadowRealm]]). + JS::GlobalInitializeCallback hostInitializeShadowRealm = + cx->runtime()->getShadowRealmInitializeGlobalCallback(); + if (hostInitializeShadowRealm) { + if (!hostInitializeShadowRealm(cx, global)) { + return false; + } + } + + // Step 13. Return O. + args.rval().setObject(*shadowRealmObj); + return true; +} + +// https://tc39.es/proposal-shadowrealm/#sec-validateshadowrealmobject +// (slightly modified into a cast operator too) +static ShadowRealmObject* ValidateShadowRealmObject(JSContext* cx, + Handle<Value> value) { + // Step 1. Perform ? RequireInternalSlot(O, [[ShadowRealm]]). + // Step 2. Perform ? RequireInternalSlot(O, [[ExecutionContext]]). + return UnwrapAndTypeCheckValue<ShadowRealmObject>(cx, value, [cx]() { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_SHADOW_REALM); + }); +} + +void js::ReportPotentiallyDetailedMessage(JSContext* cx, + const unsigned detailedError, + const unsigned genericError) { + Rooted<Value> exception(cx); + if (!cx->getPendingException(&exception)) { + return; + } + cx->clearPendingException(); + + JS::ErrorReportBuilder jsReport(cx); + JS::ExceptionStack exnStack(cx, exception, nullptr); + if (!jsReport.init(cx, exnStack, JS::ErrorReportBuilder::NoSideEffects)) { + cx->clearPendingException(); + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, genericError); + return; + } + + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, detailedError, + jsReport.toStringResult().c_str()); +} + +// PerformShadowRealmEval ( sourceText: a String, callerRealm: a Realm Record, +// evalRealm: a Realm Record, ) +// +// https://tc39.es/proposal-shadowrealm/#sec-performshadowrealmeval +static bool PerformShadowRealmEval(JSContext* cx, Handle<JSString*> sourceText, + Realm* callerRealm, Realm* evalRealm, + MutableHandle<Value> rval) { + MOZ_ASSERT(callerRealm != evalRealm); + + // Step 1. Perform ? HostEnsureCanCompileStrings(callerRealm, evalRealm). + if (!cx->isRuntimeCodeGenEnabled(JS::RuntimeCode::JS, sourceText)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_CSP_BLOCKED_SHADOWREALM); + return false; + } + + // Need to compile the script into the realm we will execute into. + // + // We hoist the error handling out however to ensure that errors + // are thrown from the correct realm. + bool compileSuccess = false; + bool evalSuccess = false; + + do { + Rooted<GlobalObject*> evalRealmGlobal(cx, evalRealm->maybeGlobal()); + AutoRealm ar(cx, evalRealmGlobal); + + // Step 2. Perform the following substeps in an implementation-defined + // order, possibly interleaving parsing and error detection: + // a. Let script be ParseText(! StringToCodePoints(sourceText), Script). + // b. If script is a List of errors, throw a SyntaxError exception. + // c. If script Contains ScriptBody is false, return undefined. + // d. Let body be the ScriptBody of script. + // e. If body Contains NewTarget is true, throw a SyntaxError exception. + // f. If body Contains SuperProperty is true, throw a SyntaxError + // exception. g. If body Contains SuperCall is true, throw a SyntaxError + // exception. + + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, sourceText)) { + return false; + } + SourceText<char16_t> srcBuf; + if (!srcBuf.initMaybeBorrowed(cx, linearChars)) { + return false; + } + + // Lets propagate some information into the compilation here. + // + // We may need to censor the stacks eventually, see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1770017 + RootedScript callerScript(cx); + const char* filename; + unsigned lineno; + uint32_t pcOffset; + bool mutedErrors; + DescribeScriptedCallerForCompilation(cx, &callerScript, &filename, &lineno, + &pcOffset, &mutedErrors); + + CompileOptions options(cx); + options.setIsRunOnce(true) + .setNoScriptRval(false) + .setMutedErrors(mutedErrors) + .setFileAndLine(filename, lineno); + + Rooted<Scope*> enclosing(cx, &evalRealmGlobal->emptyGlobalScope()); + RootedScript script( + cx, frontend::CompileEvalScript(cx, options, srcBuf, enclosing, + evalRealmGlobal)); + + compileSuccess = !!script; + if (!compileSuccess) { + break; + } + + // Step 3. Let strictEval be IsStrict of script. + // Step 4. Let runningContext be the running execution context. + // Step 5. Let lexEnv be NewDeclarativeEnvironment(evalRealm.[[GlobalEnv]]). + // Step 6. Let varEnv be evalRealm.[[GlobalEnv]]. + // Step 7. If strictEval is true, set varEnv to lexEnv. + // Step 8. If runningContext is not already suspended, suspend + // runningContext. Step 9. Let evalContext be a new ECMAScript code + // execution context. Step 10. Set evalContext's Function to null. Step 11. + // Set evalContext's Realm to evalRealm. Step 12. Set evalContext's + // ScriptOrModule to null. Step 13. Set evalContext's VariableEnvironment to + // varEnv. Step 14. Set evalContext's LexicalEnvironment to lexEnv. Step 15. + // Push evalContext onto the execution context stack; evalContext is + // now the running execution context. + // Step 16. Let result be EvalDeclarationInstantiation(body, varEnv, + // lexEnv, null, strictEval). + // Step 17. If result.[[Type]] is normal, then + // a. Set result to the result of evaluating body. + // Step 18. If result.[[Type]] is normal and result.[[Value]] is empty, then + // a. Set result to NormalCompletion(undefined). + + // Step 19. Suspend evalContext and remove it from the execution context + // stack. + // Step 20. Resume the context that is now on the top of the execution + // context stack as the running execution context. + Rooted<JSObject*> environment(cx, &evalRealmGlobal->lexicalEnvironment()); + evalSuccess = ExecuteKernel(cx, script, environment, + /* evalInFrame = */ NullFramePtr(), rval); + } while (false); // AutoRealm + + if (!compileSuccess) { + // Clone the exception into the current global and re-throw, as the + // exception has to come from the current global. + Rooted<Value> exception(cx); + if (!cx->getPendingException(&exception)) { + return false; + } + + // Clear our exception now that we've got it, so that we don't + // do the following call with an exception already pending. + cx->clearPendingException(); + + Rooted<Value> clonedException(cx); + if (!JS_StructuredClone(cx, exception, &clonedException, nullptr, + nullptr)) { + return false; + } + + cx->setPendingException(clonedException, ShouldCaptureStack::Always); + return false; + } + + if (!evalSuccess) { + // Step 21. If result.[[Type]] is not normal, throw a TypeError + // exception. + // + // The type error here needs to come from the calling global, so has to + // happen outside the AutoRealm above. + ReportPotentiallyDetailedMessage(cx, + JSMSG_SHADOW_REALM_EVALUATE_FAILURE_DETAIL, + JSMSG_SHADOW_REALM_EVALUATE_FAILURE); + + return false; + } + + // Wrap |rval| into the current compartment. + if (!cx->compartment()->wrap(cx, rval)) { + return false; + } + + // Step 22. Return ? GetWrappedValue(callerRealm, result.[[Value]]). + return GetWrappedValue(cx, callerRealm, rval, rval); +} + +// ShadowRealm.prototype.evaluate ( sourceText ) +// https://tc39.es/proposal-shadowrealm/#sec-shadowrealm.prototype.evaluate +static bool ShadowRealm_evaluate(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. Let O be this value. + HandleValue obj = args.thisv(); + + // Step 2. Perform ? ValidateShadowRealmObject(O) + Rooted<ShadowRealmObject*> shadowRealm(cx, + ValidateShadowRealmObject(cx, obj)); + if (!shadowRealm) { + return false; + } + + // Step 3. If Type(sourceText) is not String, throw a TypeError exception. + if (!args.get(0).isString()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_SHADOW_REALM_EVALUATE_NOT_STRING); + return false; + } + Rooted<JSString*> sourceText(cx, args.get(0).toString()); + + // Step 4. Let callerRealm be the current Realm Record. + Realm* callerRealm = cx->realm(); + + // Step 5. Let evalRealm be O.[[ShadowRealm]]. + Realm* evalRealm = shadowRealm->getShadowRealm(); + // Step 6. Return ? PerformShadowRealmEval(sourceText, callerRealm, + // evalRealm). + return PerformShadowRealmEval(cx, sourceText, callerRealm, evalRealm, + args.rval()); +} + +enum class ImportValueIndices : uint32_t { + CalleRealm = 0, + + ExportNameString, + + Length, +}; + +// MG:XXX: Cribbed/Overlapping with StartDynamicModuleImport; may need to +// refactor to share. +// https://tc39.es/proposal-shadowrealm/#sec-shadowrealmimportvalue +static JSObject* ShadowRealmImportValue(JSContext* cx, + Handle<JSString*> specifierString, + Handle<JSString*> exportName, + Realm* callerRealm, Realm* evalRealm) { + // Step 1. Assert: evalContext is an execution context associated to a + // ShadowRealm instance's [[ExecutionContext]]. + + // Step 2. Let innerCapability be ! NewPromiseCapability(%Promise%). + Rooted<JSObject*> promiseConstructor(cx, JS::GetPromiseConstructor(cx)); + if (!promiseConstructor) { + return nullptr; + } + + Rooted<JSObject*> promiseObject(cx, JS::NewPromiseObject(cx, nullptr)); + if (!promiseObject) { + return nullptr; + } + + Handle<PromiseObject*> promise = promiseObject.as<PromiseObject>(); + + JS::ModuleDynamicImportHook importHook = + cx->runtime()->moduleDynamicImportHook; + + if (!importHook) { + // Dynamic import can be disabled by a pref and is not supported in all + // contexts (e.g. web workers). + JS_ReportErrorASCII( + cx, + "Dynamic module import is disabled or not supported in this context"); + if (!RejectPromiseWithPendingError(cx, promise)) { + return nullptr; + } + return promise; + } + + { + // Step 3. Let runningContext be the running execution context. (Implicit) + // Step 4. If runningContext is not already suspended, suspend + // runningContext. (Implicit) + // Step 5. Push evalContext onto the execution context stack; evalContext is + // now the running execution context. (Implicit) + Rooted<GlobalObject*> evalRealmGlobal(cx, evalRealm->maybeGlobal()); + AutoRealm ar(cx, evalRealmGlobal); + + // Not Speced: Get referencing private to pass to importHook. + RootedScript script(cx); + const char* filename; + unsigned lineno; + uint32_t pcOffset; + bool mutedErrors; + DescribeScriptedCallerForCompilation(cx, &script, &filename, &lineno, + &pcOffset, &mutedErrors); + + MOZ_ASSERT(script); + + Rooted<Value> referencingPrivate(cx, script->sourceObject()->getPrivate()); + cx->runtime()->addRefScriptPrivate(referencingPrivate); + + Rooted<JSAtom*> specifierAtom(cx, AtomizeString(cx, specifierString)); + if (!specifierAtom) { + if (!RejectPromiseWithPendingError(cx, promise)) { + return nullptr; + } + return promise; + } + + Rooted<ArrayObject*> assertionArray(cx); + Rooted<JSObject*> moduleRequest( + cx, ModuleRequestObject::create(cx, specifierAtom, assertionArray)); + if (!moduleRequest) { + if (!RejectPromiseWithPendingError(cx, promise)) { + return nullptr; + } + return promise; + } + + // Step 6. Perform ! HostImportModuleDynamically(null, specifierString, + // innerCapability). + // + // By specification, this is supposed to take ReferencingScriptOrModule as + // null, see first parameter above. However, if we do that, we don't end up + // with a script reference, which is used to figure out what the base-URI + // should be So then we end up using the default one for the module loader; + // which because of the way we set the parent module loader up, means we end + // up having the incorrect base URI, as the module loader ends up just using + // the document's base URI. + // + // I have filed https://github.com/tc39/proposal-shadowrealm/issues/363 to + // discuss this. + if (!importHook(cx, referencingPrivate, moduleRequest, promise)) { + cx->runtime()->releaseScriptPrivate(referencingPrivate); + + // If there's no exception pending then the script is terminating + // anyway, so just return nullptr. + if (!cx->isExceptionPending() || + !RejectPromiseWithPendingError(cx, promise)) { + return nullptr; + } + return promise; + } + + // Step 7. Suspend evalContext and remove it from the execution context + // stack. (Implicit) + // Step 8. Resume the context that is now on the top of the execution + // context stack as the running execution context (Implicit) + } + + // Step 9. Let steps be the steps of an ExportGetter function as described + // below. + // Step 10. Let onFulfilled be ! CreateBuiltinFunction(steps, 1, "", « + // [[ExportNameString]] », callerRealm). + + // The handler can only hold onto a single object, so we pack that into a new + // array, and store there. + Rooted<ArrayObject*> handlerObject( + cx, + NewDenseFullyAllocatedArray(cx, uint32_t(ImportValueIndices::Length))); + if (!handlerObject) { + return nullptr; + } + + handlerObject->setDenseInitializedLength( + uint32_t(ImportValueIndices::Length)); + handlerObject->initDenseElement(uint32_t(ImportValueIndices::CalleRealm), + PrivateValue(callerRealm)); + handlerObject->initDenseElement( + uint32_t(ImportValueIndices::ExportNameString), StringValue(exportName)); + + Rooted<JSFunction*> onFulfilled( + cx, + NewHandlerWithExtra( + cx, + [](JSContext* cx, unsigned argc, Value* vp) { + // This is the export getter function from + // https://tc39.es/proposal-shadowrealm/#sec-shadowrealmimportvalue + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + auto* handlerObject = ExtraFromHandler<ArrayObject>(args); + + Rooted<Value> realmValue( + cx, handlerObject->getDenseElement( + uint32_t(ImportValueIndices::CalleRealm))); + Rooted<Value> exportNameValue( + cx, handlerObject->getDenseElement( + uint32_t(ImportValueIndices::ExportNameString))); + + // Step 1. Assert: exports is a module namespace exotic object. + Handle<Value> exportsValue = args[0]; + MOZ_ASSERT(exportsValue.isObject() && + exportsValue.toObject().is<ModuleNamespaceObject>()); + + Rooted<ModuleNamespaceObject*> exports( + cx, &exportsValue.toObject().as<ModuleNamespaceObject>()); + + // Step 2. Let f be the active function object. (not implemented + // this way) + // + // Step 3. Let string be f.[[ExportNameString]]. Step 4. + // Assert: Type(string) is String. + MOZ_ASSERT(exportNameValue.isString()); + + Rooted<JSAtom*> stringAtom( + cx, AtomizeString(cx, exportNameValue.toString())); + if (!stringAtom) { + return false; + } + Rooted<jsid> stringId(cx, AtomToId(stringAtom)); + + // Step 5. Let hasOwn be ? HasOwnProperty(exports, string). + bool hasOwn = false; + if (!HasOwnProperty(cx, exports, stringId, &hasOwn)) { + return false; + } + + // Step 6. If hasOwn is false, throw a TypeError exception. + if (!hasOwn) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_SHADOW_REALM_VALUE_NOT_EXPORTED); + return false; + } + + // Step 7. Let value be ? Get(exports, string). + Rooted<Value> value(cx); + if (!GetProperty(cx, exports, exports, stringId, &value)) { + return false; + } + + // Step 8. Let realm be f.[[Realm]]. + Realm* callerRealm = static_cast<Realm*>(realmValue.toPrivate()); + + // Step 9. Return ? GetWrappedValue(realm, value). + return GetWrappedValue(cx, callerRealm, value, args.rval()); + }, + promise, handlerObject)); + if (!onFulfilled) { + return nullptr; + } + + Rooted<JSFunction*> onRejected( + cx, NewHandler( + cx, + [](JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_SHADOW_REALM_IMPORTVALUE_FAILED); + return false; + }, + promise)); + if (!onRejected) { + return nullptr; + } + + // Step 11. Set onFulfilled.[[ExportNameString]] to exportNameString. + // Step 12. Let promiseCapability be ! NewPromiseCapability(%Promise%). + // Step 13. Return ! PerformPromiseThen(innerCapability.[[Promise]], + // onFulfilled, callerRealm.[[Intrinsics]].[[%ThrowTypeError%]], + // promiseCapability). + return OriginalPromiseThen(cx, promise, onFulfilled, onRejected); +} + +// ShadowRealm.prototype.importValue ( specifier, exportName ) +// https://tc39.es/proposal-shadowrealm/#sec-shadowrealm.prototype.importvalue +static bool ShadowRealm_importValue(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. Let O be this value. + HandleValue obj = args.thisv(); + + // Step 2. Perform ? ValidateShadowRealmObject(O). + Rooted<ShadowRealmObject*> shadowRealm(cx, + ValidateShadowRealmObject(cx, obj)); + if (!shadowRealm) { + return false; + } + + // Step 3. Let specifierString be ? ToString(specifier). + Rooted<JSString*> specifierString(cx, ToString<CanGC>(cx, args.get(0))); + if (!specifierString) { + return false; + } + + // Step 4. If Type(exportName) is not String, throw a TypeError exception. + if (!args.get(1).isString()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_SHADOW_REALM_EXPORT_NOT_STRING); + return false; + } + + Rooted<JSString*> exportName(cx, args.get(1).toString()); + if (!exportName) { + return false; + } + + // Step 5. Let callerRealm be the current Realm Record. + Realm* callerRealm = cx->realm(); + + // Step 6. Let evalRealm be O.[[ShadowRealm]]. + Realm* evalRealm = shadowRealm->getShadowRealm(); + + // Step 7. Let evalContext be O.[[ExecutionContext]] + // (we dont' pass this explicitly, instead using the realm+global to + // represent) + + // Step 8. Return ? + // ShadowRealmImportValue(specifierString, exportName, + // callerRealm, evalRealm, + // evalContext). + + JSObject* res = ShadowRealmImportValue(cx, specifierString, exportName, + callerRealm, evalRealm); + if (!res) { + return false; + } + + args.rval().set(ObjectValue(*res)); + return true; +} + +static const JSFunctionSpec shadowrealm_methods[] = { + JS_FN("evaluate", ShadowRealm_evaluate, 1, 0), + JS_FN("importValue", ShadowRealm_importValue, 2, 0), + JS_FS_END, +}; + +static const JSPropertySpec shadowrealm_properties[] = { + JS_STRING_SYM_PS(toStringTag, "ShadowRealm", JSPROP_READONLY), + JS_PS_END, +}; + +static const ClassSpec ShadowRealmObjectClassSpec = { + GenericCreateConstructor<ShadowRealmObject::construct, 0, + gc::AllocKind::FUNCTION>, + GenericCreatePrototype<ShadowRealmObject>, + nullptr, // Static methods + nullptr, // Static properties + shadowrealm_methods, // Methods + shadowrealm_properties, // Properties +}; + +const JSClass ShadowRealmObject::class_ = { + "ShadowRealm", + JSCLASS_HAS_CACHED_PROTO(JSProto_ShadowRealm) | + JSCLASS_HAS_RESERVED_SLOTS(ShadowRealmObject::SlotCount), + JS_NULL_CLASS_OPS, + &ShadowRealmObjectClassSpec, +}; + +const JSClass ShadowRealmObject::protoClass_ = { + "ShadowRealm.prototype", + JSCLASS_HAS_CACHED_PROTO(JSProto_ShadowRealm), + JS_NULL_CLASS_OPS, + &ShadowRealmObjectClassSpec, +}; diff --git a/js/src/builtin/ShadowRealm.h b/js/src/builtin/ShadowRealm.h new file mode 100644 index 0000000000..cdf7c034db --- /dev/null +++ b/js/src/builtin/ShadowRealm.h @@ -0,0 +1,38 @@ +/* -*- 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 builtin_ShadowRealm_h +#define builtin_ShadowRealm_h + +#include "vm/NativeObject.h" + +namespace js { + +class ShadowRealmObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass protoClass_; + + enum { GlobalSlot, SlotCount }; + + static bool construct(JSContext* cx, unsigned argc, Value* vp); + + Realm* getShadowRealm() { + MOZ_ASSERT(getWrappedGlobal()); + return getWrappedGlobal()->nonCCWRealm(); + } + + JSObject* getWrappedGlobal() const { + return &getFixedSlot(GlobalSlot).toObject(); + } +}; + +void ReportPotentiallyDetailedMessage(JSContext* cx, + const unsigned detailedError, + const unsigned genericError); +} // namespace js + +#endif diff --git a/js/src/builtin/Sorting.js b/js/src/builtin/Sorting.js new file mode 100644 index 0000000000..faa31349c0 --- /dev/null +++ b/js/src/builtin/Sorting.js @@ -0,0 +1,215 @@ +/* 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/. */ + +// We use varying sorts across the self-hosted codebase. All sorts are +// consolidated here to avoid confusion and re-implementation of existing +// algorithms. + +// For sorting small arrays. +function InsertionSort(array, from, to, comparefn) { + let item, swap, i, j; + for (i = from + 1; i <= to; i++) { + item = array[i]; + for (j = i - 1; j >= from; j--) { + swap = array[j]; + if (callContentFunction(comparefn, undefined, swap, item) <= 0) { + break; + } + array[j + 1] = swap; + } + array[j + 1] = item; + } +} + +// A helper function for MergeSort. +// +// Merge comparefn-sorted slices list[start..<=mid] and list[mid+1..<=end], +// storing the merged sequence in out[start..<=end]. +function Merge(list, out, start, mid, end, comparefn) { + // Skip lopsided runs to avoid doing useless work. + // Skip calling the comparator if the sub-list is already sorted. + if ( + mid >= end || + callContentFunction(comparefn, undefined, list[mid], list[mid + 1]) <= 0 + ) { + for (var i = start; i <= end; i++) { + DefineDataProperty(out, i, list[i]); + } + return; + } + + var i = start; + var j = mid + 1; + var k = start; + while (i <= mid && j <= end) { + var lvalue = list[i]; + var rvalue = list[j]; + if (callContentFunction(comparefn, undefined, lvalue, rvalue) <= 0) { + DefineDataProperty(out, k++, lvalue); + i++; + } else { + DefineDataProperty(out, k++, rvalue); + j++; + } + } + + // Empty out any remaining elements. + while (i <= mid) { + DefineDataProperty(out, k++, list[i++]); + } + while (j <= end) { + DefineDataProperty(out, k++, list[j++]); + } +} + +// Helper function for overwriting a sparse array with a +// dense array, filling remaining slots with holes. +function MoveHoles(sparse, sparseLen, dense, denseLen) { + for (var i = 0; i < denseLen; i++) { + sparse[i] = dense[i]; + } + for (var j = denseLen; j < sparseLen; j++) { + delete sparse[j]; + } +} + +// Iterative, bottom up, mergesort. +function MergeSort(array, len, comparefn) { + assert(IsPackedArray(array), "array is packed"); + assert(array.length === len, "length mismatch"); + assert(len > 0, "array should be non-empty"); + + // Insertion sort for small arrays, where "small" is defined by performance + // testing. + if (len < 24) { + InsertionSort(array, 0, len - 1, comparefn); + return array; + } + + // We do all of our allocating up front + var lBuffer = array; + var rBuffer = []; + + // Use insertion sort for initial ranges. + var windowSize = 4; + for (var start = 0; start < len - 1; start += windowSize) { + var end = std_Math_min(start + windowSize - 1, len - 1); + InsertionSort(lBuffer, start, end, comparefn); + } + + for (; windowSize < len; windowSize = 2 * windowSize) { + for (var start = 0; start < len; start += 2 * windowSize) { + // The midpoint between the two subarrays. + var mid = start + windowSize - 1; + + // To keep from going over the edge. + var end = std_Math_min(start + 2 * windowSize - 1, len - 1); + + Merge(lBuffer, rBuffer, start, mid, end, comparefn); + } + + // Swap both lists. + var swap = lBuffer; + lBuffer = rBuffer; + rBuffer = swap; + } + return lBuffer; +} + +// A helper function for MergeSortTypedArray. +// +// Merge comparefn-sorted slices list[start..<=mid] and list[mid+1..<=end], +// storing the merged sequence in out[start..<=end]. +function MergeTypedArray(list, out, start, mid, end, comparefn) { + // Skip lopsided runs to avoid doing useless work. + // Skip calling the comparator if the sub-list is already sorted. + if ( + mid >= end || + callContentFunction(comparefn, undefined, list[mid], list[mid + 1]) <= 0 + ) { + for (var i = start; i <= end; i++) { + out[i] = list[i]; + } + return; + } + + var i = start; + var j = mid + 1; + var k = start; + while (i <= mid && j <= end) { + var lvalue = list[i]; + var rvalue = list[j]; + if (callContentFunction(comparefn, undefined, lvalue, rvalue) <= 0) { + out[k++] = lvalue; + i++; + } else { + out[k++] = rvalue; + j++; + } + } + + // Empty out any remaining elements. + while (i <= mid) { + out[k++] = list[i++]; + } + while (j <= end) { + out[k++] = list[j++]; + } +} + +// Iterative, bottom up, mergesort. Optimized version for TypedArrays. +function MergeSortTypedArray(array, len, comparefn) { + assert( + IsPossiblyWrappedTypedArray(array), + "MergeSortTypedArray works only with typed arrays." + ); + + // Use the same TypedArray kind for the buffer. + var C = ConstructorForTypedArray(array); + + var lBuffer = new C(len); + + // Copy all elements into a temporary buffer, so that any modifications + // when calling |comparefn| are ignored. + for (var i = 0; i < len; i++) { + lBuffer[i] = array[i]; + } + + // Insertion sort for small arrays, where "small" is defined by performance + // testing. + if (len < 8) { + InsertionSort(lBuffer, 0, len - 1, comparefn); + + return lBuffer; + } + + // We do all of our allocating up front. + var rBuffer = new C(len); + + // Use insertion sort for the initial ranges. + var windowSize = 4; + for (var start = 0; start < len - 1; start += windowSize) { + var end = std_Math_min(start + windowSize - 1, len - 1); + InsertionSort(lBuffer, start, end, comparefn); + } + + for (; windowSize < len; windowSize = 2 * windowSize) { + for (var start = 0; start < len; start += 2 * windowSize) { + // The midpoint between the two subarrays. + var mid = start + windowSize - 1; + + // To keep from going over the edge. + var end = std_Math_min(start + 2 * windowSize - 1, len - 1); + + MergeTypedArray(lBuffer, rBuffer, start, mid, end, comparefn); + } + + // Swap both lists. + var swap = lBuffer; + lBuffer = rBuffer; + rBuffer = swap; + } + + return lBuffer; +} diff --git a/js/src/builtin/String.cpp b/js/src/builtin/String.cpp new file mode 100644 index 0000000000..5cee7a68bf --- /dev/null +++ b/js/src/builtin/String.cpp @@ -0,0 +1,4617 @@ +/* -*- 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 "builtin/String.h" + +#include "mozilla/Attributes.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/FloatingPoint.h" +#if JS_HAS_INTL_API +# include "mozilla/intl/String.h" +#endif +#include "mozilla/PodOperations.h" +#include "mozilla/Range.h" +#include "mozilla/SIMD.h" +#include "mozilla/TextUtils.h" + +#include <algorithm> +#include <limits> +#include <string.h> +#include <type_traits> + +#include "jsnum.h" +#include "jstypes.h" + +#include "builtin/Array.h" +#if JS_HAS_INTL_API +# include "builtin/intl/CommonFunctions.h" +# include "builtin/intl/FormatBuffer.h" +#endif +#include "builtin/RegExp.h" +#include "jit/InlinableNatives.h" +#include "js/Conversions.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#if !JS_HAS_INTL_API +# include "js/LocaleSensitive.h" +#endif +#include "js/Printer.h" +#include "js/PropertyAndElement.h" // JS_DefineFunctions +#include "js/PropertySpec.h" +#include "js/StableStringChars.h" +#include "js/UniquePtr.h" +#include "util/StringBuffer.h" +#include "util/Unicode.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/RegExpObject.h" +#include "vm/SelfHosting.h" +#include "vm/StaticStrings.h" +#include "vm/ToSource.h" // js::ValueToSource +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/GeckoProfiler-inl.h" +#include "vm/InlineCharBuffer-inl.h" +#include "vm/NativeObject-inl.h" +#include "vm/StringObject-inl.h" +#include "vm/StringType-inl.h" + +using namespace js; + +using JS::Symbol; +using JS::SymbolCode; + +using mozilla::AsciiAlphanumericToNumber; +using mozilla::CheckedInt; +using mozilla::EnsureUtf16ValiditySpan; +using mozilla::IsAsciiHexDigit; +using mozilla::PodCopy; +using mozilla::RangedPtr; +using mozilla::SIMD; +using mozilla::Span; +using mozilla::Utf16ValidUpTo; + +using JS::AutoCheckCannotGC; +using JS::AutoStableStringChars; + +static JSLinearString* ArgToLinearString(JSContext* cx, const CallArgs& args, + unsigned argno) { + if (argno >= args.length()) { + return cx->names().undefined; + } + + JSString* str = ToString<CanGC>(cx, args[argno]); + if (!str) { + return nullptr; + } + + return str->ensureLinear(cx); +} + +/* + * Forward declarations for URI encode/decode and helper routines + */ +static bool str_decodeURI(JSContext* cx, unsigned argc, Value* vp); + +static bool str_decodeURI_Component(JSContext* cx, unsigned argc, Value* vp); + +static bool str_encodeURI(JSContext* cx, unsigned argc, Value* vp); + +static bool str_encodeURI_Component(JSContext* cx, unsigned argc, Value* vp); + +/* + * Global string methods + */ + +/* ES5 B.2.1 */ +template <typename CharT> +static bool Escape(JSContext* cx, const CharT* chars, uint32_t length, + InlineCharBuffer<Latin1Char>& newChars, + uint32_t* newLengthOut) { + // clang-format off + static const uint8_t shouldPassThrough[128] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,1,1,0,1,1,1, /* !"#$%&'()*+,-./ */ + 1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0, /* 0123456789:;<=>? */ + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* @ABCDEFGHIJKLMNO */ + 1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1, /* PQRSTUVWXYZ[\]^_ */ + 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* `abcdefghijklmno */ + 1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0, /* pqrstuvwxyz{\}~ DEL */ + }; + // clang-format on + + /* Take a first pass and see how big the result string will need to be. */ + uint32_t newLength = length; + for (size_t i = 0; i < length; i++) { + char16_t ch = chars[i]; + if (ch < 128 && shouldPassThrough[ch]) { + continue; + } + + /* + * newlength is incremented below by at most 5 and at this point it must + * be a valid string length, so this should never overflow uint32_t. + */ + static_assert(JSString::MAX_LENGTH < UINT32_MAX - 5, + "Adding 5 to valid string length should not overflow"); + + MOZ_ASSERT(newLength <= JSString::MAX_LENGTH); + + /* The character will be encoded as %XX or %uXXXX. */ + newLength += (ch < 256) ? 2 : 5; + + if (MOZ_UNLIKELY(newLength > JSString::MAX_LENGTH)) { + ReportAllocationOverflow(cx); + return false; + } + } + + if (newLength == length) { + *newLengthOut = newLength; + return true; + } + + if (!newChars.maybeAlloc(cx, newLength)) { + return false; + } + + static const char digits[] = "0123456789ABCDEF"; + + Latin1Char* rawNewChars = newChars.get(); + size_t i, ni; + for (i = 0, ni = 0; i < length; i++) { + char16_t ch = chars[i]; + if (ch < 128 && shouldPassThrough[ch]) { + rawNewChars[ni++] = ch; + } else if (ch < 256) { + rawNewChars[ni++] = '%'; + rawNewChars[ni++] = digits[ch >> 4]; + rawNewChars[ni++] = digits[ch & 0xF]; + } else { + rawNewChars[ni++] = '%'; + rawNewChars[ni++] = 'u'; + rawNewChars[ni++] = digits[ch >> 12]; + rawNewChars[ni++] = digits[(ch & 0xF00) >> 8]; + rawNewChars[ni++] = digits[(ch & 0xF0) >> 4]; + rawNewChars[ni++] = digits[ch & 0xF]; + } + } + MOZ_ASSERT(ni == newLength); + + *newLengthOut = newLength; + return true; +} + +static bool str_escape(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "escape"); + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<JSLinearString*> str(cx, ArgToLinearString(cx, args, 0)); + if (!str) { + return false; + } + + InlineCharBuffer<Latin1Char> newChars; + uint32_t newLength = 0; // initialize to silence GCC warning + if (str->hasLatin1Chars()) { + AutoCheckCannotGC nogc; + if (!Escape(cx, str->latin1Chars(nogc), str->length(), newChars, + &newLength)) { + return false; + } + } else { + AutoCheckCannotGC nogc; + if (!Escape(cx, str->twoByteChars(nogc), str->length(), newChars, + &newLength)) { + return false; + } + } + + // Return input if no characters need to be escaped. + if (newLength == str->length()) { + args.rval().setString(str); + return true; + } + + JSString* res = newChars.toString(cx, newLength); + if (!res) { + return false; + } + + args.rval().setString(res); + return true; +} + +template <typename CharT> +static inline bool Unhex4(const RangedPtr<const CharT> chars, + char16_t* result) { + CharT a = chars[0], b = chars[1], c = chars[2], d = chars[3]; + + if (!(IsAsciiHexDigit(a) && IsAsciiHexDigit(b) && IsAsciiHexDigit(c) && + IsAsciiHexDigit(d))) { + return false; + } + + char16_t unhex = AsciiAlphanumericToNumber(a); + unhex = (unhex << 4) + AsciiAlphanumericToNumber(b); + unhex = (unhex << 4) + AsciiAlphanumericToNumber(c); + unhex = (unhex << 4) + AsciiAlphanumericToNumber(d); + *result = unhex; + return true; +} + +template <typename CharT> +static inline bool Unhex2(const RangedPtr<const CharT> chars, + char16_t* result) { + CharT a = chars[0], b = chars[1]; + + if (!(IsAsciiHexDigit(a) && IsAsciiHexDigit(b))) { + return false; + } + + *result = (AsciiAlphanumericToNumber(a) << 4) + AsciiAlphanumericToNumber(b); + return true; +} + +template <typename CharT> +static bool Unescape(StringBuffer& sb, + const mozilla::Range<const CharT> chars) { + // Step 2. + uint32_t length = chars.length(); + + /* + * Note that the spec algorithm has been optimized to avoid building + * a string in the case where no escapes are present. + */ + bool building = false; + +#define ENSURE_BUILDING \ + do { \ + if (!building) { \ + building = true; \ + if (!sb.reserve(length)) return false; \ + sb.infallibleAppend(chars.begin().get(), k); \ + } \ + } while (false); + + // Step 4. + uint32_t k = 0; + + // Step 5. + while (k < length) { + // Step 5.a. + char16_t c = chars[k]; + + // Step 5.b. + if (c == '%') { + static_assert(JSString::MAX_LENGTH < UINT32_MAX - 6, + "String length is not near UINT32_MAX"); + + // Steps 5.b.i-ii. + if (k + 6 <= length && chars[k + 1] == 'u') { + if (Unhex4(chars.begin() + k + 2, &c)) { + ENSURE_BUILDING + k += 5; + } + } else if (k + 3 <= length) { + if (Unhex2(chars.begin() + k + 1, &c)) { + ENSURE_BUILDING + k += 2; + } + } + } + + // Step 5.c. + if (building && !sb.append(c)) { + return false; + } + + // Step 5.d. + k += 1; + } + + return true; +#undef ENSURE_BUILDING +} + +// ES2018 draft rev f83aa38282c2a60c6916ebc410bfdf105a0f6a54 +// B.2.1.2 unescape ( string ) +static bool str_unescape(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "unescape"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<JSLinearString*> str(cx, ArgToLinearString(cx, args, 0)); + if (!str) { + return false; + } + + // Step 3. + JSStringBuilder sb(cx); + if (str->hasTwoByteChars() && !sb.ensureTwoByteChars()) { + return false; + } + + // Steps 2, 4-5. + bool unescapeFailed = false; + if (str->hasLatin1Chars()) { + AutoCheckCannotGC nogc; + unescapeFailed = !Unescape(sb, str->latin1Range(nogc)); + } else { + AutoCheckCannotGC nogc; + unescapeFailed = !Unescape(sb, str->twoByteRange(nogc)); + } + if (unescapeFailed) { + return false; + } + + // Step 6. + JSLinearString* result; + if (!sb.empty()) { + result = sb.finishString(); + if (!result) { + return false; + } + } else { + result = str; + } + + args.rval().setString(result); + return true; +} + +static bool str_uneval(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + JSString* str = ValueToSource(cx, args.get(0)); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static const JSFunctionSpec string_functions[] = { + JS_FN(js_escape_str, str_escape, 1, JSPROP_RESOLVING), + JS_FN(js_unescape_str, str_unescape, 1, JSPROP_RESOLVING), + JS_FN(js_uneval_str, str_uneval, 1, JSPROP_RESOLVING), + JS_FN(js_decodeURI_str, str_decodeURI, 1, JSPROP_RESOLVING), + JS_FN(js_encodeURI_str, str_encodeURI, 1, JSPROP_RESOLVING), + JS_FN(js_decodeURIComponent_str, str_decodeURI_Component, 1, + JSPROP_RESOLVING), + JS_FN(js_encodeURIComponent_str, str_encodeURI_Component, 1, + JSPROP_RESOLVING), + + JS_FS_END}; + +static const unsigned STRING_ELEMENT_ATTRS = + JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT; + +static bool str_enumerate(JSContext* cx, HandleObject obj) { + RootedString str(cx, obj->as<StringObject>().unbox()); + js::StaticStrings& staticStrings = cx->staticStrings(); + + RootedValue value(cx); + for (size_t i = 0, length = str->length(); i < length; i++) { + JSString* str1 = staticStrings.getUnitStringForElement(cx, str, i); + if (!str1) { + return false; + } + value.setString(str1); + if (!DefineDataElement(cx, obj, i, value, + STRING_ELEMENT_ATTRS | JSPROP_RESOLVING)) { + return false; + } + } + + return true; +} + +static bool str_mayResolve(const JSAtomState&, jsid id, JSObject*) { + // str_resolve ignores non-integer ids. + return id.isInt(); +} + +static bool str_resolve(JSContext* cx, HandleObject obj, HandleId id, + bool* resolvedp) { + if (!id.isInt()) { + return true; + } + + RootedString str(cx, obj->as<StringObject>().unbox()); + + int32_t slot = id.toInt(); + if ((size_t)slot < str->length()) { + JSString* str1 = + cx->staticStrings().getUnitStringForElement(cx, str, size_t(slot)); + if (!str1) { + return false; + } + RootedValue value(cx, StringValue(str1)); + if (!DefineDataElement(cx, obj, uint32_t(slot), value, + STRING_ELEMENT_ATTRS | JSPROP_RESOLVING)) { + return false; + } + *resolvedp = true; + } + return true; +} + +static const JSClassOps StringObjectClassOps = { + nullptr, // addProperty + nullptr, // delProperty + str_enumerate, // enumerate + nullptr, // newEnumerate + str_resolve, // resolve + str_mayResolve, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass StringObject::class_ = { + js_String_str, + JSCLASS_HAS_RESERVED_SLOTS(StringObject::RESERVED_SLOTS) | + JSCLASS_HAS_CACHED_PROTO(JSProto_String), + &StringObjectClassOps, &StringObject::classSpec_}; + +/* + * Perform the initial |RequireObjectCoercible(thisv)| and |ToString(thisv)| + * from nearly all String.prototype.* functions. + */ +static MOZ_ALWAYS_INLINE JSString* ToStringForStringFunction( + JSContext* cx, const char* funName, HandleValue thisv) { + if (thisv.isString()) { + return thisv.toString(); + } + + if (thisv.isObject()) { + if (thisv.toObject().is<StringObject>()) { + StringObject* nobj = &thisv.toObject().as<StringObject>(); + // We have to make sure that the ToPrimitive call from ToString + // would be unobservable. + if (HasNoToPrimitiveMethodPure(nobj, cx) && + HasNativeMethodPure(nobj, cx->names().toString, str_toString, cx)) { + return nobj->unbox(); + } + } + } else if (thisv.isNullOrUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_PROTO, "String", funName, + thisv.isNull() ? "null" : "undefined"); + return nullptr; + } + + return ToStringSlow<CanGC>(cx, thisv); +} + +MOZ_ALWAYS_INLINE bool IsString(HandleValue v) { + return v.isString() || (v.isObject() && v.toObject().is<StringObject>()); +} + +MOZ_ALWAYS_INLINE bool str_toSource_impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsString(args.thisv())); + + JSString* str = ToString<CanGC>(cx, args.thisv()); + if (!str) { + return false; + } + + UniqueChars quoted = QuoteString(cx, str, '"'); + if (!quoted) { + return false; + } + + JSStringBuilder sb(cx); + if (!sb.append("(new String(") || + !sb.append(quoted.get(), strlen(quoted.get())) || !sb.append("))")) { + return false; + } + + JSString* result = sb.finishString(); + if (!result) { + return false; + } + args.rval().setString(result); + return true; +} + +static bool str_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsString, str_toSource_impl>(cx, args); +} + +MOZ_ALWAYS_INLINE bool str_toString_impl(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsString(args.thisv())); + + args.rval().setString( + args.thisv().isString() + ? args.thisv().toString() + : args.thisv().toObject().as<StringObject>().unbox()); + return true; +} + +bool js::str_toString(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsString, str_toString_impl>(cx, args); +} + +/* + * Java-like string native methods. + */ + +JSString* js::SubstringKernel(JSContext* cx, HandleString str, int32_t beginInt, + int32_t lengthInt) { + MOZ_ASSERT(0 <= beginInt); + MOZ_ASSERT(0 <= lengthInt); + MOZ_ASSERT(uint32_t(beginInt) <= str->length()); + MOZ_ASSERT(uint32_t(lengthInt) <= str->length() - beginInt); + + uint32_t begin = beginInt; + uint32_t len = lengthInt; + + /* + * Optimization for one level deep ropes. + * This is common for the following pattern: + * + * while() { + * text = text.substr(0, x) + "bla" + text.substr(x) + * test.charCodeAt(x + 1) + * } + */ + if (str->isRope()) { + JSRope* rope = &str->asRope(); + + /* Substring is totally in leftChild of rope. */ + if (begin + len <= rope->leftChild()->length()) { + return NewDependentString(cx, rope->leftChild(), begin, len); + } + + /* Substring is totally in rightChild of rope. */ + if (begin >= rope->leftChild()->length()) { + begin -= rope->leftChild()->length(); + return NewDependentString(cx, rope->rightChild(), begin, len); + } + + /* + * Requested substring is partly in the left and partly in right child. + * Create a rope of substrings for both childs. + */ + MOZ_ASSERT(begin < rope->leftChild()->length() && + begin + len > rope->leftChild()->length()); + + size_t lhsLength = rope->leftChild()->length() - begin; + size_t rhsLength = begin + len - rope->leftChild()->length(); + + Rooted<JSRope*> ropeRoot(cx, rope); + RootedString lhs( + cx, NewDependentString(cx, ropeRoot->leftChild(), begin, lhsLength)); + if (!lhs) { + return nullptr; + } + + RootedString rhs( + cx, NewDependentString(cx, ropeRoot->rightChild(), 0, rhsLength)); + if (!rhs) { + return nullptr; + } + + return JSRope::new_<CanGC>(cx, lhs, rhs, len); + } + + return NewDependentString(cx, str, begin, len); +} + +/** + * U+03A3 GREEK CAPITAL LETTER SIGMA has two different lower case mappings + * depending on its context: + * When it's preceded by a cased character and not followed by another cased + * character, its lower case form is U+03C2 GREEK SMALL LETTER FINAL SIGMA. + * Otherwise its lower case mapping is U+03C3 GREEK SMALL LETTER SIGMA. + * + * Unicode 9.0, §3.13 Default Case Algorithms + */ +static char16_t Final_Sigma(const char16_t* chars, size_t length, + size_t index) { + MOZ_ASSERT(index < length); + MOZ_ASSERT(chars[index] == unicode::GREEK_CAPITAL_LETTER_SIGMA); + MOZ_ASSERT(unicode::ToLowerCase(unicode::GREEK_CAPITAL_LETTER_SIGMA) == + unicode::GREEK_SMALL_LETTER_SIGMA); + +#if JS_HAS_INTL_API + // Tell the analysis the BinaryProperty.contains function pointer called by + // mozilla::intl::String::Is{CaseIgnorable, Cased} cannot GC. + JS::AutoSuppressGCAnalysis nogc; + + bool precededByCased = false; + for (size_t i = index; i > 0;) { + char16_t c = chars[--i]; + char32_t codePoint = c; + if (unicode::IsTrailSurrogate(c) && i > 0) { + char16_t lead = chars[i - 1]; + if (unicode::IsLeadSurrogate(lead)) { + codePoint = unicode::UTF16Decode(lead, c); + i--; + } + } + + // Ignore any characters with the property Case_Ignorable. + // NB: We need to skip over all Case_Ignorable characters, even when + // they also have the Cased binary property. + if (mozilla::intl::String::IsCaseIgnorable(codePoint)) { + continue; + } + + precededByCased = mozilla::intl::String::IsCased(codePoint); + break; + } + if (!precededByCased) { + return unicode::GREEK_SMALL_LETTER_SIGMA; + } + + bool followedByCased = false; + for (size_t i = index + 1; i < length;) { + char16_t c = chars[i++]; + char32_t codePoint = c; + if (unicode::IsLeadSurrogate(c) && i < length) { + char16_t trail = chars[i]; + if (unicode::IsTrailSurrogate(trail)) { + codePoint = unicode::UTF16Decode(c, trail); + i++; + } + } + + // Ignore any characters with the property Case_Ignorable. + // NB: We need to skip over all Case_Ignorable characters, even when + // they also have the Cased binary property. + if (mozilla::intl::String::IsCaseIgnorable(codePoint)) { + continue; + } + + followedByCased = mozilla::intl::String::IsCased(codePoint); + break; + } + if (!followedByCased) { + return unicode::GREEK_SMALL_LETTER_FINAL_SIGMA; + } +#endif + + return unicode::GREEK_SMALL_LETTER_SIGMA; +} + +// If |srcLength == destLength| is true, the destination buffer was allocated +// with the same size as the source buffer. When we append characters which +// have special casing mappings, we test |srcLength == destLength| to decide +// if we need to back out and reallocate a sufficiently large destination +// buffer. Otherwise the destination buffer was allocated with the correct +// size to hold all lower case mapped characters, i.e. +// |destLength == ToLowerCaseLength(srcChars, 0, srcLength)| is true. +template <typename CharT> +static size_t ToLowerCaseImpl(CharT* destChars, const CharT* srcChars, + size_t startIndex, size_t srcLength, + size_t destLength) { + MOZ_ASSERT(startIndex < srcLength); + MOZ_ASSERT(srcLength <= destLength); + if constexpr (std::is_same_v<CharT, Latin1Char>) { + MOZ_ASSERT(srcLength == destLength); + } + + size_t j = startIndex; + for (size_t i = startIndex; i < srcLength; i++) { + CharT c = srcChars[i]; + if constexpr (!std::is_same_v<CharT, Latin1Char>) { + if (unicode::IsLeadSurrogate(c) && i + 1 < srcLength) { + char16_t trail = srcChars[i + 1]; + if (unicode::IsTrailSurrogate(trail)) { + trail = unicode::ToLowerCaseNonBMPTrail(c, trail); + destChars[j++] = c; + destChars[j++] = trail; + i++; + continue; + } + } + + // Special case: U+0130 LATIN CAPITAL LETTER I WITH DOT ABOVE + // lowercases to <U+0069 U+0307>. + if (c == unicode::LATIN_CAPITAL_LETTER_I_WITH_DOT_ABOVE) { + // Return if the output buffer is too small. + if (srcLength == destLength) { + return i; + } + + destChars[j++] = CharT('i'); + destChars[j++] = CharT(unicode::COMBINING_DOT_ABOVE); + continue; + } + + // Special case: U+03A3 GREEK CAPITAL LETTER SIGMA lowercases to + // one of two codepoints depending on context. + if (c == unicode::GREEK_CAPITAL_LETTER_SIGMA) { + destChars[j++] = Final_Sigma(srcChars, srcLength, i); + continue; + } + } + + c = unicode::ToLowerCase(c); + destChars[j++] = c; + } + + MOZ_ASSERT(j == destLength); + return srcLength; +} + +static size_t ToLowerCaseLength(const char16_t* chars, size_t startIndex, + size_t length) { + size_t lowerLength = length; + for (size_t i = startIndex; i < length; i++) { + char16_t c = chars[i]; + + // U+0130 is lowercased to the two-element sequence <U+0069 U+0307>. + if (c == unicode::LATIN_CAPITAL_LETTER_I_WITH_DOT_ABOVE) { + lowerLength += 1; + } + } + return lowerLength; +} + +template <typename CharT> +static JSString* ToLowerCase(JSContext* cx, JSLinearString* str) { + // Unlike toUpperCase, toLowerCase has the nice invariant that if the + // input is a Latin-1 string, the output is also a Latin-1 string. + + InlineCharBuffer<CharT> newChars; + + const size_t length = str->length(); + size_t resultLength; + { + AutoCheckCannotGC nogc; + const CharT* chars = str->chars<CharT>(nogc); + + // We don't need extra special casing checks in the loop below, + // because U+0130 LATIN CAPITAL LETTER I WITH DOT ABOVE and U+03A3 + // GREEK CAPITAL LETTER SIGMA already have simple lower case mappings. + MOZ_ASSERT(unicode::ChangesWhenLowerCased( + unicode::LATIN_CAPITAL_LETTER_I_WITH_DOT_ABOVE), + "U+0130 has a simple lower case mapping"); + MOZ_ASSERT( + unicode::ChangesWhenLowerCased(unicode::GREEK_CAPITAL_LETTER_SIGMA), + "U+03A3 has a simple lower case mapping"); + + // One element Latin-1 strings can be directly retrieved from the + // static strings cache. + if constexpr (std::is_same_v<CharT, Latin1Char>) { + if (length == 1) { + CharT lower = unicode::ToLowerCase(chars[0]); + MOZ_ASSERT(StaticStrings::hasUnit(lower)); + + return cx->staticStrings().getUnit(lower); + } + } + + // Look for the first character that changes when lowercased. + size_t i = 0; + for (; i < length; i++) { + CharT c = chars[i]; + if constexpr (!std::is_same_v<CharT, Latin1Char>) { + if (unicode::IsLeadSurrogate(c) && i + 1 < length) { + CharT trail = chars[i + 1]; + if (unicode::IsTrailSurrogate(trail)) { + if (unicode::ChangesWhenLowerCasedNonBMP(c, trail)) { + break; + } + + i++; + continue; + } + } + } + if (unicode::ChangesWhenLowerCased(c)) { + break; + } + } + + // If no character needs to change, return the input string. + if (i == length) { + return str; + } + + resultLength = length; + if (!newChars.maybeAlloc(cx, resultLength)) { + return nullptr; + } + + PodCopy(newChars.get(), chars, i); + + size_t readChars = + ToLowerCaseImpl(newChars.get(), chars, i, length, resultLength); + if constexpr (!std::is_same_v<CharT, Latin1Char>) { + if (readChars < length) { + resultLength = ToLowerCaseLength(chars, readChars, length); + + if (!newChars.maybeRealloc(cx, length, resultLength)) { + return nullptr; + } + + MOZ_ALWAYS_TRUE(length == ToLowerCaseImpl(newChars.get(), chars, + readChars, length, + resultLength)); + } + } else { + MOZ_ASSERT(readChars == length, + "Latin-1 strings don't have special lower case mappings"); + } + } + + return newChars.toStringDontDeflate(cx, resultLength); +} + +JSString* js::StringToLowerCase(JSContext* cx, HandleString string) { + JSLinearString* linear = string->ensureLinear(cx); + if (!linear) { + return nullptr; + } + + if (linear->hasLatin1Chars()) { + return ToLowerCase<Latin1Char>(cx, linear); + } + return ToLowerCase<char16_t>(cx, linear); +} + +static bool str_toLowerCase(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "toLowerCase"); + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString str(cx, + ToStringForStringFunction(cx, "toLowerCase", args.thisv())); + if (!str) { + return false; + } + + JSString* result = StringToLowerCase(cx, str); + if (!result) { + return false; + } + + args.rval().setString(result); + return true; +} + +#if JS_HAS_INTL_API +// String.prototype.toLocaleLowerCase is self-hosted when Intl is exposed, +// with core functionality performed by the intrinsic below. + +static const char* CaseMappingLocale(JSContext* cx, JSString* str) { + JSLinearString* locale = str->ensureLinear(cx); + if (!locale) { + return nullptr; + } + + MOZ_ASSERT(locale->length() >= 2, "locale is a valid language tag"); + + // Lithuanian, Turkish, and Azeri have language dependent case mappings. + static const char languagesWithSpecialCasing[][3] = {"lt", "tr", "az"}; + + // All strings in |languagesWithSpecialCasing| are of length two, so we + // only need to compare the first two characters to find a matching locale. + // ES2017 Intl, §9.2.2 BestAvailableLocale + if (locale->length() == 2 || locale->latin1OrTwoByteChar(2) == '-') { + for (const auto& language : languagesWithSpecialCasing) { + if (locale->latin1OrTwoByteChar(0) == language[0] && + locale->latin1OrTwoByteChar(1) == language[1]) { + return language; + } + } + } + + return ""; // ICU root locale +} + +static bool HasDefaultCasing(const char* locale) { return !strcmp(locale, ""); } + +bool js::intl_toLocaleLowerCase(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(args[0].isString()); + MOZ_ASSERT(args[1].isString()); + + RootedString string(cx, args[0].toString()); + + const char* locale = CaseMappingLocale(cx, args[1].toString()); + if (!locale) { + return false; + } + + // Call String.prototype.toLowerCase() for language independent casing. + if (HasDefaultCasing(locale)) { + JSString* str = StringToLowerCase(cx, string); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; + } + + AutoStableStringChars inputChars(cx); + if (!inputChars.initTwoByte(cx, string)) { + return false; + } + mozilla::Range<const char16_t> input = inputChars.twoByteRange(); + + // Note: maximum case mapping length is three characters, so the result + // length might be > INT32_MAX. ICU will fail in this case. + static_assert(JSString::MAX_LENGTH <= INT32_MAX, + "String length must fit in int32_t for ICU"); + + static const size_t INLINE_CAPACITY = js::intl::INITIAL_CHAR_BUFFER_SIZE; + + intl::FormatBuffer<char16_t, INLINE_CAPACITY> buffer(cx); + + auto ok = mozilla::intl::String::ToLocaleLowerCase(locale, input, buffer); + if (ok.isErr()) { + intl::ReportInternalError(cx, ok.unwrapErr()); + return false; + } + + JSString* result = buffer.toString(cx); + if (!result) { + return false; + } + + args.rval().setString(result); + return true; +} + +#else + +// When the Intl API is not exposed, String.prototype.toLowerCase is implemented +// in C++. +static bool str_toLocaleLowerCase(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", + "toLocaleLowerCase"); + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString str( + cx, ToStringForStringFunction(cx, "toLocaleLowerCase", args.thisv())); + if (!str) { + return false; + } + + /* + * Forcefully ignore the first (or any) argument and return toLowerCase(), + * ECMA has reserved that argument, presumably for defining the locale. + */ + if (cx->runtime()->localeCallbacks && + cx->runtime()->localeCallbacks->localeToLowerCase) { + RootedValue result(cx); + if (!cx->runtime()->localeCallbacks->localeToLowerCase(cx, str, &result)) { + return false; + } + + args.rval().set(result); + return true; + } + + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + JSString* result = StringToLowerCase(cx, linear); + if (!result) { + return false; + } + + args.rval().setString(result); + return true; +} + +#endif // JS_HAS_INTL_API + +static inline bool ToUpperCaseHasSpecialCasing(Latin1Char charCode) { + // U+00DF LATIN SMALL LETTER SHARP S is the only Latin-1 code point with + // special casing rules, so detect it inline. + bool hasUpperCaseSpecialCasing = + charCode == unicode::LATIN_SMALL_LETTER_SHARP_S; + MOZ_ASSERT(hasUpperCaseSpecialCasing == + unicode::ChangesWhenUpperCasedSpecialCasing(charCode)); + + return hasUpperCaseSpecialCasing; +} + +static inline bool ToUpperCaseHasSpecialCasing(char16_t charCode) { + return unicode::ChangesWhenUpperCasedSpecialCasing(charCode); +} + +static inline size_t ToUpperCaseLengthSpecialCasing(Latin1Char charCode) { + // U+00DF LATIN SMALL LETTER SHARP S is uppercased to two 'S'. + MOZ_ASSERT(charCode == unicode::LATIN_SMALL_LETTER_SHARP_S); + + return 2; +} + +static inline size_t ToUpperCaseLengthSpecialCasing(char16_t charCode) { + MOZ_ASSERT(ToUpperCaseHasSpecialCasing(charCode)); + + return unicode::LengthUpperCaseSpecialCasing(charCode); +} + +static inline void ToUpperCaseAppendUpperCaseSpecialCasing(char16_t charCode, + Latin1Char* elements, + size_t* index) { + // U+00DF LATIN SMALL LETTER SHARP S is uppercased to two 'S'. + MOZ_ASSERT(charCode == unicode::LATIN_SMALL_LETTER_SHARP_S); + static_assert('S' <= JSString::MAX_LATIN1_CHAR, "'S' is a Latin-1 character"); + + elements[(*index)++] = 'S'; + elements[(*index)++] = 'S'; +} + +static inline void ToUpperCaseAppendUpperCaseSpecialCasing(char16_t charCode, + char16_t* elements, + size_t* index) { + unicode::AppendUpperCaseSpecialCasing(charCode, elements, index); +} + +// See ToLowerCaseImpl for an explanation of the parameters. +template <typename DestChar, typename SrcChar> +static size_t ToUpperCaseImpl(DestChar* destChars, const SrcChar* srcChars, + size_t startIndex, size_t srcLength, + size_t destLength) { + static_assert(std::is_same_v<SrcChar, Latin1Char> || + !std::is_same_v<DestChar, Latin1Char>, + "cannot write non-Latin-1 characters into Latin-1 string"); + MOZ_ASSERT(startIndex < srcLength); + MOZ_ASSERT(srcLength <= destLength); + + size_t j = startIndex; + for (size_t i = startIndex; i < srcLength; i++) { + char16_t c = srcChars[i]; + if constexpr (!std::is_same_v<DestChar, Latin1Char>) { + if (unicode::IsLeadSurrogate(c) && i + 1 < srcLength) { + char16_t trail = srcChars[i + 1]; + if (unicode::IsTrailSurrogate(trail)) { + trail = unicode::ToUpperCaseNonBMPTrail(c, trail); + destChars[j++] = c; + destChars[j++] = trail; + i++; + continue; + } + } + } + + if (MOZ_UNLIKELY(c > 0x7f && + ToUpperCaseHasSpecialCasing(static_cast<SrcChar>(c)))) { + // Return if the output buffer is too small. + if (srcLength == destLength) { + return i; + } + + ToUpperCaseAppendUpperCaseSpecialCasing(c, destChars, &j); + continue; + } + + c = unicode::ToUpperCase(c); + if constexpr (std::is_same_v<DestChar, Latin1Char>) { + MOZ_ASSERT(c <= JSString::MAX_LATIN1_CHAR); + } + destChars[j++] = c; + } + + MOZ_ASSERT(j == destLength); + return srcLength; +} + +template <typename CharT> +static size_t ToUpperCaseLength(const CharT* chars, size_t startIndex, + size_t length) { + size_t upperLength = length; + for (size_t i = startIndex; i < length; i++) { + char16_t c = chars[i]; + + if (c > 0x7f && ToUpperCaseHasSpecialCasing(static_cast<CharT>(c))) { + upperLength += ToUpperCaseLengthSpecialCasing(static_cast<CharT>(c)) - 1; + } + } + return upperLength; +} + +template <typename DestChar, typename SrcChar> +static inline void CopyChars(DestChar* destChars, const SrcChar* srcChars, + size_t length) { + static_assert(!std::is_same_v<DestChar, SrcChar>, + "PodCopy is used for the same type case"); + for (size_t i = 0; i < length; i++) { + destChars[i] = srcChars[i]; + } +} + +template <typename CharT> +static inline void CopyChars(CharT* destChars, const CharT* srcChars, + size_t length) { + PodCopy(destChars, srcChars, length); +} + +template <typename DestChar, typename SrcChar> +static inline bool ToUpperCase(JSContext* cx, + InlineCharBuffer<DestChar>& newChars, + const SrcChar* chars, size_t startIndex, + size_t length, size_t* resultLength) { + MOZ_ASSERT(startIndex < length); + + *resultLength = length; + if (!newChars.maybeAlloc(cx, length)) { + return false; + } + + CopyChars(newChars.get(), chars, startIndex); + + size_t readChars = + ToUpperCaseImpl(newChars.get(), chars, startIndex, length, length); + if (readChars < length) { + size_t actualLength = ToUpperCaseLength(chars, readChars, length); + + *resultLength = actualLength; + if (!newChars.maybeRealloc(cx, length, actualLength)) { + return false; + } + + MOZ_ALWAYS_TRUE(length == ToUpperCaseImpl(newChars.get(), chars, readChars, + length, actualLength)); + } + + return true; +} + +template <typename CharT> +static JSString* ToUpperCase(JSContext* cx, JSLinearString* str) { + using Latin1Buffer = InlineCharBuffer<Latin1Char>; + using TwoByteBuffer = InlineCharBuffer<char16_t>; + + mozilla::MaybeOneOf<Latin1Buffer, TwoByteBuffer> newChars; + const size_t length = str->length(); + size_t resultLength; + { + AutoCheckCannotGC nogc; + const CharT* chars = str->chars<CharT>(nogc); + + // Most one element Latin-1 strings can be directly retrieved from the + // static strings cache. + if constexpr (std::is_same_v<CharT, Latin1Char>) { + if (length == 1) { + Latin1Char c = chars[0]; + if (c != unicode::MICRO_SIGN && + c != unicode::LATIN_SMALL_LETTER_Y_WITH_DIAERESIS && + c != unicode::LATIN_SMALL_LETTER_SHARP_S) { + char16_t upper = unicode::ToUpperCase(c); + MOZ_ASSERT(upper <= JSString::MAX_LATIN1_CHAR); + MOZ_ASSERT(StaticStrings::hasUnit(upper)); + + return cx->staticStrings().getUnit(upper); + } + + MOZ_ASSERT(unicode::ToUpperCase(c) > JSString::MAX_LATIN1_CHAR || + ToUpperCaseHasSpecialCasing(c)); + } + } + + // Look for the first character that changes when uppercased. + size_t i = 0; + for (; i < length; i++) { + CharT c = chars[i]; + if constexpr (!std::is_same_v<CharT, Latin1Char>) { + if (unicode::IsLeadSurrogate(c) && i + 1 < length) { + CharT trail = chars[i + 1]; + if (unicode::IsTrailSurrogate(trail)) { + if (unicode::ChangesWhenUpperCasedNonBMP(c, trail)) { + break; + } + + i++; + continue; + } + } + } + if (unicode::ChangesWhenUpperCased(c)) { + break; + } + if (MOZ_UNLIKELY(c > 0x7f && ToUpperCaseHasSpecialCasing(c))) { + break; + } + } + + // If no character needs to change, return the input string. + if (i == length) { + return str; + } + + // The string changes when uppercased, so we must create a new string. + // Can it be Latin-1? + // + // If the original string is Latin-1, it can -- unless the string + // contains U+00B5 MICRO SIGN or U+00FF SMALL LETTER Y WITH DIAERESIS, + // the only Latin-1 codepoints that don't uppercase within Latin-1. + // Search for those codepoints to decide whether the new string can be + // Latin-1. + // If the original string is a two-byte string, its uppercase form is + // so rarely Latin-1 that we don't even consider creating a new + // Latin-1 string. + if constexpr (std::is_same_v<CharT, Latin1Char>) { + bool resultIsLatin1 = true; + for (size_t j = i; j < length; j++) { + Latin1Char c = chars[j]; + if (c == unicode::MICRO_SIGN || + c == unicode::LATIN_SMALL_LETTER_Y_WITH_DIAERESIS) { + MOZ_ASSERT(unicode::ToUpperCase(c) > JSString::MAX_LATIN1_CHAR); + resultIsLatin1 = false; + break; + } else { + MOZ_ASSERT(unicode::ToUpperCase(c) <= JSString::MAX_LATIN1_CHAR); + } + } + + if (resultIsLatin1) { + newChars.construct<Latin1Buffer>(); + + if (!ToUpperCase(cx, newChars.ref<Latin1Buffer>(), chars, i, length, + &resultLength)) { + return nullptr; + } + } else { + newChars.construct<TwoByteBuffer>(); + + if (!ToUpperCase(cx, newChars.ref<TwoByteBuffer>(), chars, i, length, + &resultLength)) { + return nullptr; + } + } + } else { + newChars.construct<TwoByteBuffer>(); + + if (!ToUpperCase(cx, newChars.ref<TwoByteBuffer>(), chars, i, length, + &resultLength)) { + return nullptr; + } + } + } + + auto toString = [&](auto& chars) { + return chars.toStringDontDeflate(cx, resultLength); + }; + + return newChars.mapNonEmpty(toString); +} + +JSString* js::StringToUpperCase(JSContext* cx, HandleString string) { + JSLinearString* linear = string->ensureLinear(cx); + if (!linear) { + return nullptr; + } + + if (linear->hasLatin1Chars()) { + return ToUpperCase<Latin1Char>(cx, linear); + } + return ToUpperCase<char16_t>(cx, linear); +} + +static bool str_toUpperCase(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "toUpperCase"); + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString str(cx, + ToStringForStringFunction(cx, "toUpperCase", args.thisv())); + if (!str) { + return false; + } + + JSString* result = StringToUpperCase(cx, str); + if (!result) { + return false; + } + + args.rval().setString(result); + return true; +} + +#if JS_HAS_INTL_API +// String.prototype.toLocaleUpperCase is self-hosted when Intl is exposed, +// with core functionality performed by the intrinsic below. + +bool js::intl_toLocaleUpperCase(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(args[0].isString()); + MOZ_ASSERT(args[1].isString()); + + RootedString string(cx, args[0].toString()); + + const char* locale = CaseMappingLocale(cx, args[1].toString()); + if (!locale) { + return false; + } + + // Call String.prototype.toUpperCase() for language independent casing. + if (HasDefaultCasing(locale)) { + JSString* str = js::StringToUpperCase(cx, string); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; + } + + AutoStableStringChars inputChars(cx); + if (!inputChars.initTwoByte(cx, string)) { + return false; + } + mozilla::Range<const char16_t> input = inputChars.twoByteRange(); + + // Note: maximum case mapping length is three characters, so the result + // length might be > INT32_MAX. ICU will fail in this case. + static_assert(JSString::MAX_LENGTH <= INT32_MAX, + "String length must fit in int32_t for ICU"); + + static const size_t INLINE_CAPACITY = js::intl::INITIAL_CHAR_BUFFER_SIZE; + + intl::FormatBuffer<char16_t, INLINE_CAPACITY> buffer(cx); + + auto ok = mozilla::intl::String::ToLocaleUpperCase(locale, input, buffer); + if (ok.isErr()) { + intl::ReportInternalError(cx, ok.unwrapErr()); + return false; + } + + JSString* result = buffer.toString(cx); + if (!result) { + return false; + } + + args.rval().setString(result); + return true; +} + +#else + +// When the Intl API is not exposed, String.prototype.toUpperCase is implemented +// in C++. +static bool str_toLocaleUpperCase(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", + "toLocaleUpperCase"); + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString str( + cx, ToStringForStringFunction(cx, "toLocaleUpperCase", args.thisv())); + if (!str) { + return false; + } + + /* + * Forcefully ignore the first (or any) argument and return toUpperCase(), + * ECMA has reserved that argument, presumably for defining the locale. + */ + if (cx->runtime()->localeCallbacks && + cx->runtime()->localeCallbacks->localeToUpperCase) { + RootedValue result(cx); + if (!cx->runtime()->localeCallbacks->localeToUpperCase(cx, str, &result)) { + return false; + } + + args.rval().set(result); + return true; + } + + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + JSString* result = StringToUpperCase(cx, linear); + if (!result) { + return false; + } + + args.rval().setString(result); + return true; +} + +#endif // JS_HAS_INTL_API + +#if JS_HAS_INTL_API + +// String.prototype.localeCompare is self-hosted when Intl functionality is +// exposed, and the only intrinsics it requires are provided in the +// implementation of Intl.Collator. + +#else + +// String.prototype.localeCompare is implemented in C++ (delegating to +// JSLocaleCallbacks) when Intl functionality is not exposed. +static bool str_localeCompare(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", + "localeCompare"); + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString str( + cx, ToStringForStringFunction(cx, "localeCompare", args.thisv())); + if (!str) { + return false; + } + + RootedString thatStr(cx, ToString<CanGC>(cx, args.get(0))); + if (!thatStr) { + return false; + } + + if (cx->runtime()->localeCallbacks && + cx->runtime()->localeCallbacks->localeCompare) { + RootedValue result(cx); + if (!cx->runtime()->localeCallbacks->localeCompare(cx, str, thatStr, + &result)) { + return false; + } + + args.rval().set(result); + return true; + } + + int32_t result; + if (!CompareStrings(cx, str, thatStr, &result)) { + return false; + } + + args.rval().setInt32(result); + return true; +} + +#endif // JS_HAS_INTL_API + +#if JS_HAS_INTL_API + +// ES2017 draft rev 45e890512fd77add72cc0ee742785f9f6f6482de +// 21.1.3.12 String.prototype.normalize ( [ form ] ) +// +// String.prototype.normalize is only implementable if ICU's normalization +// functionality is available. +static bool str_normalize(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "normalize"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-2. + RootedString str(cx, + ToStringForStringFunction(cx, "normalize", args.thisv())); + if (!str) { + return false; + } + + using NormalizationForm = mozilla::intl::String::NormalizationForm; + + NormalizationForm form; + if (!args.hasDefined(0)) { + // Step 3. + form = NormalizationForm::NFC; + } else { + // Step 4. + JSLinearString* formStr = ArgToLinearString(cx, args, 0); + if (!formStr) { + return false; + } + + // Step 5. + if (EqualStrings(formStr, cx->names().NFC)) { + form = NormalizationForm::NFC; + } else if (EqualStrings(formStr, cx->names().NFD)) { + form = NormalizationForm::NFD; + } else if (EqualStrings(formStr, cx->names().NFKC)) { + form = NormalizationForm::NFKC; + } else if (EqualStrings(formStr, cx->names().NFKD)) { + form = NormalizationForm::NFKD; + } else { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_NORMALIZE_FORM); + return false; + } + } + + // Latin-1 strings are already in Normalization Form C. + if (form == NormalizationForm::NFC && str->hasLatin1Chars()) { + // Step 7. + args.rval().setString(str); + return true; + } + + // Step 6. + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, str)) { + return false; + } + + mozilla::Range<const char16_t> srcChars = stableChars.twoByteRange(); + + static const size_t INLINE_CAPACITY = js::intl::INITIAL_CHAR_BUFFER_SIZE; + + intl::FormatBuffer<char16_t, INLINE_CAPACITY> buffer(cx); + + auto alreadyNormalized = + mozilla::intl::String::Normalize(form, srcChars, buffer); + if (alreadyNormalized.isErr()) { + intl::ReportInternalError(cx, alreadyNormalized.unwrapErr()); + return false; + } + + using AlreadyNormalized = mozilla::intl::String::AlreadyNormalized; + + // Return if the input string is already normalized. + if (alreadyNormalized.unwrap() == AlreadyNormalized::Yes) { + // Step 7. + args.rval().setString(str); + return true; + } + + JSString* ns = buffer.toString(cx); + if (!ns) { + return false; + } + + // Step 7. + args.rval().setString(ns); + return true; +} + +#endif // JS_HAS_INTL_API + +#ifdef NIGHTLY_BUILD +/** + * IsStringWellFormedUnicode ( string ) + * https://tc39.es/ecma262/#sec-isstringwellformedunicode + */ +static bool IsStringWellFormedUnicode(JSContext* cx, HandleString str, + bool* isWellFormedOut) { + MOZ_ASSERT(isWellFormedOut); + *isWellFormedOut = false; + + JSLinearString* linear = str->ensureLinear(cx); + if (!linear) { + return false; + } + + // Latin1 chars are well-formed. + if (linear->hasLatin1Chars()) { + *isWellFormedOut = true; + return true; + } + + { + AutoCheckCannotGC nogc; + size_t len = linear->length(); + *isWellFormedOut = + Utf16ValidUpTo(Span{linear->twoByteChars(nogc), len}) == len; + } + return true; +} + +/** + * Well-Formed Unicode Strings (Stage 3 proposal) + * + * String.prototype.isWellFormed + * https://tc39.es/proposal-is-usv-string/#sec-string.prototype.iswellformed + */ +static bool str_isWellFormed(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "isWellFormed"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. Let O be ? RequireObjectCoercible(this value). + // Step 2. Let S be ? ToString(O). + RootedString str(cx, + ToStringForStringFunction(cx, "isWellFormed", args.thisv())); + if (!str) { + return false; + } + + // Step 3. Return IsStringWellFormedUnicode(S). + bool isWellFormed; + if (!IsStringWellFormedUnicode(cx, str, &isWellFormed)) { + return false; + } + + args.rval().setBoolean(isWellFormed); + return true; +} + +/** + * Well-Formed Unicode Strings (Stage 3 proposal) + * + * String.prototype.toWellFormed + * https://tc39.es/proposal-is-usv-string/#sec-string.prototype.towellformed + */ +static bool str_toWellFormed(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "toWellFormed"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. Let O be ? RequireObjectCoercible(this value). + // Step 2. Let S be ? ToString(O). + RootedString str(cx, + ToStringForStringFunction(cx, "toWellFormed", args.thisv())); + if (!str) { + return false; + } + + // If the string itself is well-formed, return it. + bool isWellFormed; + if (!IsStringWellFormedUnicode(cx, str, &isWellFormed)) { + return false; + } + if (isWellFormed) { + args.rval().setString(str); + return true; + } + + // Step 3. Let strLen be the length of S. + size_t len = str->length(); + + // Step 4-6 + auto buffer = cx->make_pod_arena_array<char16_t>(js::StringBufferArena, len); + if (!buffer) { + return false; + } + + { + AutoCheckCannotGC nogc; + JSLinearString* linear = str->ensureLinear(cx); + PodCopy(buffer.get(), linear->twoByteChars(nogc), len); + EnsureUtf16ValiditySpan(Span{buffer.get(), len}); + } + + JSString* result = NewString<CanGC>(cx, std::move(buffer), len); + if (!result) { + return false; + } + + // Step 7. Return result. + args.rval().setString(result); + return true; +} +#endif // NIGHTLY_BUILD + +static bool str_charAt(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "charAt"); + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString str(cx); + size_t i; + if (args.thisv().isString() && args.length() != 0 && args[0].isInt32()) { + str = args.thisv().toString(); + i = size_t(args[0].toInt32()); + if (i >= str->length()) { + goto out_of_range; + } + } else { + str = ToStringForStringFunction(cx, "charAt", args.thisv()); + if (!str) { + return false; + } + + double d = 0.0; + if (args.length() > 0 && !ToInteger(cx, args[0], &d)) { + return false; + } + + if (d < 0 || str->length() <= d) { + goto out_of_range; + } + i = size_t(d); + } + + str = cx->staticStrings().getUnitStringForElement(cx, str, i); + if (!str) { + return false; + } + args.rval().setString(str); + return true; + +out_of_range: + args.rval().setString(cx->runtime()->emptyString); + return true; +} + +bool js::str_charCodeAt_impl(JSContext* cx, HandleString string, + HandleValue index, MutableHandleValue res) { + size_t i; + if (index.isInt32()) { + i = index.toInt32(); + if (i >= string->length()) { + goto out_of_range; + } + } else { + double d = 0.0; + if (!ToInteger(cx, index, &d)) { + return false; + } + // check whether d is negative as size_t is unsigned + if (d < 0 || string->length() <= d) { + goto out_of_range; + } + i = size_t(d); + } + char16_t c; + if (!string->getChar(cx, i, &c)) { + return false; + } + res.setInt32(c); + return true; + +out_of_range: + res.setNaN(); + return true; +} + +bool js::str_charCodeAt(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "charCodeAt"); + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString str(cx); + RootedValue index(cx); + if (args.thisv().isString()) { + str = args.thisv().toString(); + } else { + str = ToStringForStringFunction(cx, "charCodeAt", args.thisv()); + if (!str) { + return false; + } + } + if (args.length() != 0) { + index = args[0]; + } else { + index.setInt32(0); + } + + return js::str_charCodeAt_impl(cx, str, index, args.rval()); +} + +/* + * Boyer-Moore-Horspool superlinear search for pat:patlen in text:textlen. + * The patlen argument must be positive and no greater than sBMHPatLenMax. + * + * Return the index of pat in text, or -1 if not found. + */ +static const uint32_t sBMHCharSetSize = 256; /* ISO-Latin-1 */ +static const uint32_t sBMHPatLenMax = 255; /* skip table element is uint8_t */ +static const int sBMHBadPattern = + -2; /* return value if pat is not ISO-Latin-1 */ + +template <typename TextChar, typename PatChar> +static int BoyerMooreHorspool(const TextChar* text, uint32_t textLen, + const PatChar* pat, uint32_t patLen) { + MOZ_ASSERT(0 < patLen && patLen <= sBMHPatLenMax); + + uint8_t skip[sBMHCharSetSize]; + for (uint32_t i = 0; i < sBMHCharSetSize; i++) { + skip[i] = uint8_t(patLen); + } + + uint32_t patLast = patLen - 1; + for (uint32_t i = 0; i < patLast; i++) { + char16_t c = pat[i]; + if (c >= sBMHCharSetSize) { + return sBMHBadPattern; + } + skip[c] = uint8_t(patLast - i); + } + + for (uint32_t k = patLast; k < textLen;) { + for (uint32_t i = k, j = patLast;; i--, j--) { + if (text[i] != pat[j]) { + break; + } + if (j == 0) { + return static_cast<int>(i); /* safe: max string size */ + } + } + + char16_t c = text[k]; + k += (c >= sBMHCharSetSize) ? patLen : skip[c]; + } + return -1; +} + +template <typename TextChar, typename PatChar> +struct MemCmp { + using Extent = uint32_t; + static MOZ_ALWAYS_INLINE Extent computeExtent(const PatChar*, + uint32_t patLen) { + return (patLen - 2) * sizeof(PatChar); + } + static MOZ_ALWAYS_INLINE bool match(const PatChar* p, const TextChar* t, + Extent extent) { + MOZ_ASSERT(sizeof(TextChar) == sizeof(PatChar)); + return memcmp(p, t, extent) == 0; + } +}; + +template <typename TextChar, typename PatChar> +struct ManualCmp { + using Extent = const PatChar*; + static MOZ_ALWAYS_INLINE Extent computeExtent(const PatChar* pat, + uint32_t patLen) { + return pat + patLen; + } + static MOZ_ALWAYS_INLINE bool match(const PatChar* p, const TextChar* t, + Extent extent) { + for (; p != extent; ++p, ++t) { + if (*p != *t) { + return false; + } + } + return true; + } +}; + +template <class InnerMatch, typename TextChar, typename PatChar> +static int Matcher(const TextChar* text, uint32_t textlen, const PatChar* pat, + uint32_t patlen) { + MOZ_ASSERT(patlen > 1); + + const typename InnerMatch::Extent extent = + InnerMatch::computeExtent(pat, patlen); + + uint32_t i = 0; + uint32_t n = textlen - patlen + 1; + + while (i < n) { + const TextChar* pos; + + // This is a bit awkward. Consider the case where we're searching "abcdef" + // for "def". n will be 4, because we know in advance that the last place we + // can *start* a successful search will be at 'd'. However, if we just use n + // - i, then our first search will be looking through "abcd" for "de", + // because our memchr2xN functions search for two characters at a time. So + // we just have to compensate by adding 1. This will never exceed textlen + // because we know patlen is at least two. + size_t searchLen = n - i + 1; + if (sizeof(TextChar) == 1) { + MOZ_ASSERT(pat[0] <= 0xff); + pos = (TextChar*)SIMD::memchr2x8((char*)text + i, pat[0], pat[1], + searchLen); + } else { + pos = (TextChar*)SIMD::memchr2x16((char16_t*)(text + i), char16_t(pat[0]), + char16_t(pat[1]), searchLen); + } + + if (pos == nullptr) { + return -1; + } + + i = static_cast<uint32_t>(pos - text); + const uint32_t inlineLookaheadChars = 2; + if (InnerMatch::match(pat + inlineLookaheadChars, + text + i + inlineLookaheadChars, extent)) { + return i; + } + + i += 1; + } + return -1; +} + +template <typename TextChar, typename PatChar> +static MOZ_ALWAYS_INLINE int StringMatch(const TextChar* text, uint32_t textLen, + const PatChar* pat, uint32_t patLen) { + if (patLen == 0) { + return 0; + } + if (textLen < patLen) { + return -1; + } + + if (sizeof(TextChar) == 1 && sizeof(PatChar) > 1 && pat[0] > 0xff) { + return -1; + } + + if (patLen == 1) { + const TextChar* pos; + if (sizeof(TextChar) == 1) { + MOZ_ASSERT(pat[0] <= 0xff); + pos = (TextChar*)SIMD::memchr8((char*)text, pat[0], textLen); + } else { + pos = + (TextChar*)SIMD::memchr16((char16_t*)text, char16_t(pat[0]), textLen); + } + + if (pos == nullptr) { + return -1; + } + + return pos - text; + } + + // We use a fast two-character-wide search in Matcher below, so we need to + // validate that pat[1] isn't outside the latin1 range up front if the + // sizes are different. + if (sizeof(TextChar) == 1 && sizeof(PatChar) > 1 && pat[1] > 0xff) { + return -1; + } + + /* + * If the text or pattern string is short, BMH will be more expensive than + * the basic linear scan due to initialization cost and a more complex loop + * body. While the correct threshold is input-dependent, we can make a few + * conservative observations: + * - When |textLen| is "big enough", the initialization time will be + * proportionally small, so the worst-case slowdown is minimized. + * - When |patLen| is "too small", even the best case for BMH will be + * slower than a simple scan for large |textLen| due to the more complex + * loop body of BMH. + * From this, the values for "big enough" and "too small" are determined + * empirically. See bug 526348. + */ + if (textLen >= 512 && patLen >= 11 && patLen <= sBMHPatLenMax) { + int index = BoyerMooreHorspool(text, textLen, pat, patLen); + if (index != sBMHBadPattern) { + return index; + } + } + + /* + * For big patterns with large potential overlap we want the SIMD-optimized + * speed of memcmp. For small patterns, a simple loop is faster. We also can't + * use memcmp if one of the strings is TwoByte and the other is Latin-1. + */ + return (patLen > 128 && std::is_same_v<TextChar, PatChar>) + ? Matcher<MemCmp<TextChar, PatChar>, TextChar, PatChar>( + text, textLen, pat, patLen) + : Matcher<ManualCmp<TextChar, PatChar>, TextChar, PatChar>( + text, textLen, pat, patLen); +} + +static int32_t StringMatch(JSLinearString* text, JSLinearString* pat, + uint32_t start = 0) { + MOZ_ASSERT(start <= text->length()); + uint32_t textLen = text->length() - start; + uint32_t patLen = pat->length(); + + int match; + AutoCheckCannotGC nogc; + if (text->hasLatin1Chars()) { + const Latin1Char* textChars = text->latin1Chars(nogc) + start; + if (pat->hasLatin1Chars()) { + match = StringMatch(textChars, textLen, pat->latin1Chars(nogc), patLen); + } else { + match = StringMatch(textChars, textLen, pat->twoByteChars(nogc), patLen); + } + } else { + const char16_t* textChars = text->twoByteChars(nogc) + start; + if (pat->hasLatin1Chars()) { + match = StringMatch(textChars, textLen, pat->latin1Chars(nogc), patLen); + } else { + match = StringMatch(textChars, textLen, pat->twoByteChars(nogc), patLen); + } + } + + return (match == -1) ? -1 : start + match; +} + +static const size_t sRopeMatchThresholdRatioLog2 = 4; + +int js::StringFindPattern(JSLinearString* text, JSLinearString* pat, + size_t start) { + return StringMatch(text, pat, start); +} + +typedef Vector<JSLinearString*, 16, SystemAllocPolicy> LinearStringVector; + +template <typename TextChar, typename PatChar> +static int RopeMatchImpl(const AutoCheckCannotGC& nogc, + LinearStringVector& strings, const PatChar* pat, + size_t patLen) { + /* Absolute offset from the beginning of the logical text string. */ + int pos = 0; + + for (JSLinearString** outerp = strings.begin(); outerp != strings.end(); + ++outerp) { + /* Try to find a match within 'outer'. */ + JSLinearString* outer = *outerp; + const TextChar* chars = outer->chars<TextChar>(nogc); + size_t len = outer->length(); + int matchResult = StringMatch(chars, len, pat, patLen); + if (matchResult != -1) { + /* Matched! */ + return pos + matchResult; + } + + /* Try to find a match starting in 'outer' and running into other nodes. */ + const TextChar* const text = chars + (patLen > len ? 0 : len - patLen + 1); + const TextChar* const textend = chars + len; + const PatChar p0 = *pat; + const PatChar* const p1 = pat + 1; + const PatChar* const patend = pat + patLen; + for (const TextChar* t = text; t != textend;) { + if (*t++ != p0) { + continue; + } + + JSLinearString** innerp = outerp; + const TextChar* ttend = textend; + const TextChar* tt = t; + for (const PatChar* pp = p1; pp != patend; ++pp, ++tt) { + while (tt == ttend) { + if (++innerp == strings.end()) { + return -1; + } + + JSLinearString* inner = *innerp; + tt = inner->chars<TextChar>(nogc); + ttend = tt + inner->length(); + } + if (*pp != *tt) { + goto break_continue; + } + } + + /* Matched! */ + return pos + (t - chars) - 1; /* -1 because of *t++ above */ + + break_continue:; + } + + pos += len; + } + + return -1; +} + +/* + * RopeMatch takes the text to search and the pattern to search for in the text. + * RopeMatch returns false on OOM and otherwise returns the match index through + * the 'match' outparam (-1 for not found). + */ +static bool RopeMatch(JSContext* cx, JSRope* text, JSLinearString* pat, + int* match) { + uint32_t patLen = pat->length(); + if (patLen == 0) { + *match = 0; + return true; + } + if (text->length() < patLen) { + *match = -1; + return true; + } + + /* + * List of leaf nodes in the rope. If we run out of memory when trying to + * append to this list, we can still fall back to StringMatch, so use the + * system allocator so we don't report OOM in that case. + */ + LinearStringVector strings; + + /* + * We don't want to do rope matching if there is a poor node-to-char ratio, + * since this means spending a lot of time in the match loop below. We also + * need to build the list of leaf nodes. Do both here: iterate over the + * nodes so long as there are not too many. + * + * We also don't use rope matching if the rope contains both Latin-1 and + * TwoByte nodes, to simplify the match algorithm. + */ + { + size_t threshold = text->length() >> sRopeMatchThresholdRatioLog2; + StringSegmentRange r(cx); + if (!r.init(text)) { + return false; + } + + bool textIsLatin1 = text->hasLatin1Chars(); + while (!r.empty()) { + if (threshold-- == 0 || r.front()->hasLatin1Chars() != textIsLatin1 || + !strings.append(r.front())) { + JSLinearString* linear = text->ensureLinear(cx); + if (!linear) { + return false; + } + + *match = StringMatch(linear, pat); + return true; + } + if (!r.popFront()) { + return false; + } + } + } + + AutoCheckCannotGC nogc; + if (text->hasLatin1Chars()) { + if (pat->hasLatin1Chars()) { + *match = RopeMatchImpl<Latin1Char>(nogc, strings, pat->latin1Chars(nogc), + patLen); + } else { + *match = RopeMatchImpl<Latin1Char>(nogc, strings, pat->twoByteChars(nogc), + patLen); + } + } else { + if (pat->hasLatin1Chars()) { + *match = RopeMatchImpl<char16_t>(nogc, strings, pat->latin1Chars(nogc), + patLen); + } else { + *match = RopeMatchImpl<char16_t>(nogc, strings, pat->twoByteChars(nogc), + patLen); + } + } + + return true; +} + +static MOZ_ALWAYS_INLINE bool ReportErrorIfFirstArgIsRegExp( + JSContext* cx, const CallArgs& args) { + // Only call IsRegExp if the first argument is definitely an object, so we + // don't pay the cost of an additional function call in the common case. + if (args.length() == 0 || !args[0].isObject()) { + return true; + } + + bool isRegExp; + if (!IsRegExp(cx, args[0], &isRegExp)) { + return false; + } + + if (isRegExp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_ARG_TYPE, "first", "", + "Regular Expression"); + return false; + } + return true; +} + +// ES2018 draft rev de77aaeffce115deaf948ed30c7dbe4c60983c0c +// 21.1.3.7 String.prototype.includes ( searchString [ , position ] ) +bool js::str_includes(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "includes"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-2. + RootedString str(cx, ToStringForStringFunction(cx, "includes", args.thisv())); + if (!str) { + return false; + } + + // Steps 3-4. + if (!ReportErrorIfFirstArgIsRegExp(cx, args)) { + return false; + } + + // Step 5. + Rooted<JSLinearString*> searchStr(cx, ArgToLinearString(cx, args, 0)); + if (!searchStr) { + return false; + } + + // Step 6. + uint32_t pos = 0; + if (args.hasDefined(1)) { + if (args[1].isInt32()) { + int i = args[1].toInt32(); + pos = (i < 0) ? 0U : uint32_t(i); + } else { + double d; + if (!ToInteger(cx, args[1], &d)) { + return false; + } + pos = uint32_t(std::min(std::max(d, 0.0), double(UINT32_MAX))); + } + } + + // Step 7. + uint32_t textLen = str->length(); + + // Step 8. + uint32_t start = std::min(pos, textLen); + + // Steps 9-10. + JSLinearString* text = str->ensureLinear(cx); + if (!text) { + return false; + } + + args.rval().setBoolean(StringMatch(text, searchStr, start) != -1); + return true; +} + +/* ES6 20120927 draft 15.5.4.7. */ +bool js::str_indexOf(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "indexOf"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1, 2, and 3 + RootedString str(cx, ToStringForStringFunction(cx, "indexOf", args.thisv())); + if (!str) { + return false; + } + + // Steps 4 and 5 + Rooted<JSLinearString*> searchStr(cx, ArgToLinearString(cx, args, 0)); + if (!searchStr) { + return false; + } + + // Steps 6 and 7 + uint32_t pos = 0; + if (args.hasDefined(1)) { + if (args[1].isInt32()) { + int i = args[1].toInt32(); + pos = (i < 0) ? 0U : uint32_t(i); + } else { + double d; + if (!ToInteger(cx, args[1], &d)) { + return false; + } + pos = uint32_t(std::min(std::max(d, 0.0), double(UINT32_MAX))); + } + } + + // Step 8 + uint32_t textLen = str->length(); + + // Step 9 + uint32_t start = std::min(pos, textLen); + + if (str == searchStr) { + // AngularJS often invokes "false".indexOf("false"). This check should + // be cheap enough to not hurt anything else. + args.rval().setInt32(start == 0 ? 0 : -1); + return true; + } + + // Steps 10 and 11 + JSLinearString* text = str->ensureLinear(cx); + if (!text) { + return false; + } + + args.rval().setInt32(StringMatch(text, searchStr, start)); + return true; +} + +bool js::StringIndexOf(JSContext* cx, HandleString string, + HandleString searchString, int32_t* result) { + if (string == searchString) { + *result = 0; + return true; + } + + JSLinearString* text = string->ensureLinear(cx); + if (!text) { + return false; + } + + JSLinearString* searchStr = searchString->ensureLinear(cx); + if (!searchStr) { + return false; + } + + *result = StringMatch(text, searchStr, 0); + return true; +} + +template <typename TextChar, typename PatChar> +static int32_t LastIndexOfImpl(const TextChar* text, size_t textLen, + const PatChar* pat, size_t patLen, + size_t start) { + MOZ_ASSERT(patLen > 0); + MOZ_ASSERT(patLen <= textLen); + MOZ_ASSERT(start <= textLen - patLen); + + const PatChar p0 = *pat; + const PatChar* patNext = pat + 1; + const PatChar* patEnd = pat + patLen; + + for (const TextChar* t = text + start; t >= text; --t) { + if (*t == p0) { + const TextChar* t1 = t + 1; + for (const PatChar* p1 = patNext; p1 < patEnd; ++p1, ++t1) { + if (*t1 != *p1) { + goto break_continue; + } + } + + return static_cast<int32_t>(t - text); + } + break_continue:; + } + + return -1; +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 21.1.3.9 String.prototype.lastIndexOf ( searchString [ , position ] ) +static bool str_lastIndexOf(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "lastIndexOf"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-2. + RootedString str(cx, + ToStringForStringFunction(cx, "lastIndexOf", args.thisv())); + if (!str) { + return false; + } + + // Step 3. + Rooted<JSLinearString*> searchStr(cx, ArgToLinearString(cx, args, 0)); + if (!searchStr) { + return false; + } + + // Step 6. + size_t len = str->length(); + + // Step 8. + size_t searchLen = searchStr->length(); + + // Steps 4-5, 7. + int start = len - searchLen; // Start searching here + if (args.hasDefined(1)) { + if (args[1].isInt32()) { + int i = args[1].toInt32(); + if (i <= 0) { + start = 0; + } else if (i < start) { + start = i; + } + } else { + double d; + if (!ToNumber(cx, args[1], &d)) { + return false; + } + if (!std::isnan(d)) { + d = JS::ToInteger(d); + if (d <= 0) { + start = 0; + } else if (d < start) { + start = int(d); + } + } + } + } + + if (str == searchStr) { + args.rval().setInt32(0); + return true; + } + + if (searchLen > len) { + args.rval().setInt32(-1); + return true; + } + + if (searchLen == 0) { + args.rval().setInt32(start); + return true; + } + MOZ_ASSERT(0 <= start && size_t(start) < len); + + JSLinearString* text = str->ensureLinear(cx); + if (!text) { + return false; + } + + // Step 9. + int32_t res; + AutoCheckCannotGC nogc; + if (text->hasLatin1Chars()) { + const Latin1Char* textChars = text->latin1Chars(nogc); + if (searchStr->hasLatin1Chars()) { + res = LastIndexOfImpl(textChars, len, searchStr->latin1Chars(nogc), + searchLen, start); + } else { + res = LastIndexOfImpl(textChars, len, searchStr->twoByteChars(nogc), + searchLen, start); + } + } else { + const char16_t* textChars = text->twoByteChars(nogc); + if (searchStr->hasLatin1Chars()) { + res = LastIndexOfImpl(textChars, len, searchStr->latin1Chars(nogc), + searchLen, start); + } else { + res = LastIndexOfImpl(textChars, len, searchStr->twoByteChars(nogc), + searchLen, start); + } + } + + args.rval().setInt32(res); + return true; +} + +// ES2018 draft rev de77aaeffce115deaf948ed30c7dbe4c60983c0c +// 21.1.3.20 String.prototype.startsWith ( searchString [ , position ] ) +bool js::str_startsWith(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "startsWith"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-2. + RootedString str(cx, + ToStringForStringFunction(cx, "startsWith", args.thisv())); + if (!str) { + return false; + } + + // Steps 3-4. + if (!ReportErrorIfFirstArgIsRegExp(cx, args)) { + return false; + } + + // Step 5. + Rooted<JSLinearString*> searchStr(cx, ArgToLinearString(cx, args, 0)); + if (!searchStr) { + return false; + } + + // Step 6. + uint32_t pos = 0; + if (args.hasDefined(1)) { + if (args[1].isInt32()) { + int i = args[1].toInt32(); + pos = (i < 0) ? 0U : uint32_t(i); + } else { + double d; + if (!ToInteger(cx, args[1], &d)) { + return false; + } + pos = uint32_t(std::min(std::max(d, 0.0), double(UINT32_MAX))); + } + } + + // Step 7. + uint32_t textLen = str->length(); + + // Step 8. + uint32_t start = std::min(pos, textLen); + + // Step 9. + uint32_t searchLen = searchStr->length(); + + // Step 10. + if (searchLen + start < searchLen || searchLen + start > textLen) { + args.rval().setBoolean(false); + return true; + } + + // Steps 11-12. + JSLinearString* text = str->ensureLinear(cx); + if (!text) { + return false; + } + + args.rval().setBoolean(HasSubstringAt(text, searchStr, start)); + return true; +} + +bool js::StringStartsWith(JSContext* cx, HandleString string, + HandleString searchString, bool* result) { + if (searchString->length() > string->length()) { + *result = false; + return true; + } + + JSLinearString* str = string->ensureLinear(cx); + if (!str) { + return false; + } + + JSLinearString* searchStr = searchString->ensureLinear(cx); + if (!searchStr) { + return false; + } + + *result = HasSubstringAt(str, searchStr, 0); + return true; +} + +// ES2018 draft rev de77aaeffce115deaf948ed30c7dbe4c60983c0c +// 21.1.3.6 String.prototype.endsWith ( searchString [ , endPosition ] ) +bool js::str_endsWith(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "endsWith"); + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-2. + RootedString str(cx, ToStringForStringFunction(cx, "endsWith", args.thisv())); + if (!str) { + return false; + } + + // Steps 3-4. + if (!ReportErrorIfFirstArgIsRegExp(cx, args)) { + return false; + } + + // Step 5. + Rooted<JSLinearString*> searchStr(cx, ArgToLinearString(cx, args, 0)); + if (!searchStr) { + return false; + } + + // Step 6. + uint32_t textLen = str->length(); + + // Step 7. + uint32_t pos = textLen; + if (args.hasDefined(1)) { + if (args[1].isInt32()) { + int i = args[1].toInt32(); + pos = (i < 0) ? 0U : uint32_t(i); + } else { + double d; + if (!ToInteger(cx, args[1], &d)) { + return false; + } + pos = uint32_t(std::min(std::max(d, 0.0), double(UINT32_MAX))); + } + } + + // Step 8. + uint32_t end = std::min(pos, textLen); + + // Step 9. + uint32_t searchLen = searchStr->length(); + + // Step 11 (reordered). + if (searchLen > end) { + args.rval().setBoolean(false); + return true; + } + + // Step 10. + uint32_t start = end - searchLen; + + // Steps 12-13. + JSLinearString* text = str->ensureLinear(cx); + if (!text) { + return false; + } + + args.rval().setBoolean(HasSubstringAt(text, searchStr, start)); + return true; +} + +bool js::StringEndsWith(JSContext* cx, HandleString string, + HandleString searchString, bool* result) { + if (searchString->length() > string->length()) { + *result = false; + return true; + } + + JSLinearString* str = string->ensureLinear(cx); + if (!str) { + return false; + } + + JSLinearString* searchStr = searchString->ensureLinear(cx); + if (!searchStr) { + return false; + } + + uint32_t start = str->length() - searchStr->length(); + + *result = HasSubstringAt(str, searchStr, start); + return true; +} + +template <typename CharT> +static void TrimString(const CharT* chars, bool trimStart, bool trimEnd, + size_t length, size_t* pBegin, size_t* pEnd) { + size_t begin = 0, end = length; + + if (trimStart) { + while (begin < length && unicode::IsSpace(chars[begin])) { + ++begin; + } + } + + if (trimEnd) { + while (end > begin && unicode::IsSpace(chars[end - 1])) { + --end; + } + } + + *pBegin = begin; + *pEnd = end; +} + +static bool TrimString(JSContext* cx, const CallArgs& args, const char* funName, + bool trimStart, bool trimEnd) { + JSString* str = ToStringForStringFunction(cx, funName, args.thisv()); + if (!str) { + return false; + } + + JSLinearString* linear = str->ensureLinear(cx); + if (!linear) { + return false; + } + + size_t length = linear->length(); + size_t begin, end; + if (linear->hasLatin1Chars()) { + AutoCheckCannotGC nogc; + TrimString(linear->latin1Chars(nogc), trimStart, trimEnd, length, &begin, + &end); + } else { + AutoCheckCannotGC nogc; + TrimString(linear->twoByteChars(nogc), trimStart, trimEnd, length, &begin, + &end); + } + + JSLinearString* result = NewDependentString(cx, linear, begin, end - begin); + if (!result) { + return false; + } + + args.rval().setString(result); + return true; +} + +static bool str_trim(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "trim"); + CallArgs args = CallArgsFromVp(argc, vp); + return TrimString(cx, args, "trim", true, true); +} + +static bool str_trimStart(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "trimStart"); + CallArgs args = CallArgsFromVp(argc, vp); + return TrimString(cx, args, "trimStart", true, false); +} + +static bool str_trimEnd(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "trimEnd"); + CallArgs args = CallArgsFromVp(argc, vp); + return TrimString(cx, args, "trimEnd", false, true); +} + +// Utility for building a rope (lazy concatenation) of strings. +class RopeBuilder { + JSContext* cx; + RootedString res; + + RopeBuilder(const RopeBuilder& other) = delete; + void operator=(const RopeBuilder& other) = delete; + + public: + explicit RopeBuilder(JSContext* cx) + : cx(cx), res(cx, cx->runtime()->emptyString) {} + + inline bool append(HandleString str) { + res = ConcatStrings<CanGC>(cx, res, str); + return !!res; + } + + inline JSString* result() { return res; } +}; + +namespace { + +template <typename CharT> +static uint32_t FindDollarIndex(const CharT* chars, size_t length) { + if (const CharT* p = js_strchr_limit(chars, '$', chars + length)) { + uint32_t dollarIndex = p - chars; + MOZ_ASSERT(dollarIndex < length); + return dollarIndex; + } + return UINT32_MAX; +} + +} /* anonymous namespace */ + +/* + * Constructs a result string that looks like: + * + * newstring = string[:matchStart] + repstr + string[matchEnd:] + */ +static JSString* BuildFlatReplacement(JSContext* cx, HandleString textstr, + Handle<JSLinearString*> repstr, + size_t matchStart, size_t patternLength) { + size_t matchEnd = matchStart + patternLength; + + RootedString resultStr(cx, NewDependentString(cx, textstr, 0, matchStart)); + if (!resultStr) { + return nullptr; + } + + resultStr = ConcatStrings<CanGC>(cx, resultStr, repstr); + if (!resultStr) { + return nullptr; + } + + MOZ_ASSERT(textstr->length() >= matchEnd); + RootedString rest(cx, NewDependentString(cx, textstr, matchEnd, + textstr->length() - matchEnd)); + if (!rest) { + return nullptr; + } + + return ConcatStrings<CanGC>(cx, resultStr, rest); +} + +static JSString* BuildFlatRopeReplacement(JSContext* cx, HandleString textstr, + Handle<JSLinearString*> repstr, + size_t match, size_t patternLength) { + MOZ_ASSERT(textstr->isRope()); + + size_t matchEnd = match + patternLength; + + /* + * If we are replacing over a rope, avoid flattening it by iterating + * through it, building a new rope. + */ + StringSegmentRange r(cx); + if (!r.init(textstr)) { + return nullptr; + } + + RopeBuilder builder(cx); + + /* + * Special case when the pattern string is '', which matches to the + * head of the string and doesn't overlap with any component of the rope. + */ + if (patternLength == 0) { + MOZ_ASSERT(match == 0); + if (!builder.append(repstr)) { + return nullptr; + } + } + + size_t pos = 0; + while (!r.empty()) { + RootedString str(cx, r.front()); + size_t len = str->length(); + size_t strEnd = pos + len; + if (pos < matchEnd && strEnd > match) { + /* + * We need to special-case any part of the rope that overlaps + * with the replacement string. + */ + if (match >= pos) { + /* + * If this part of the rope overlaps with the left side of + * the pattern, then it must be the only one to overlap with + * the first character in the pattern, so we include the + * replacement string here. + */ + RootedString leftSide(cx, NewDependentString(cx, str, 0, match - pos)); + if (!leftSide || !builder.append(leftSide) || !builder.append(repstr)) { + return nullptr; + } + } + + /* + * If str runs off the end of the matched string, append the + * last part of str. + */ + if (strEnd > matchEnd) { + RootedString rightSide( + cx, NewDependentString(cx, str, matchEnd - pos, strEnd - matchEnd)); + if (!rightSide || !builder.append(rightSide)) { + return nullptr; + } + } + } else { + if (!builder.append(str)) { + return nullptr; + } + } + pos += str->length(); + if (!r.popFront()) { + return nullptr; + } + } + + return builder.result(); +} + +template <typename CharT> +static bool AppendDollarReplacement(StringBuffer& newReplaceChars, + size_t firstDollarIndex, size_t matchStart, + size_t matchLimit, JSLinearString* text, + const CharT* repChars, size_t repLength) { + MOZ_ASSERT(firstDollarIndex < repLength); + MOZ_ASSERT(matchStart <= matchLimit); + MOZ_ASSERT(matchLimit <= text->length()); + + // Move the pre-dollar chunk in bulk. + if (!newReplaceChars.append(repChars, firstDollarIndex)) { + return false; + } + + // Move the rest char-by-char, interpreting dollars as we encounter them. + const CharT* repLimit = repChars + repLength; + for (const CharT* it = repChars + firstDollarIndex; it < repLimit; ++it) { + if (*it != '$' || it == repLimit - 1) { + if (!newReplaceChars.append(*it)) { + return false; + } + continue; + } + + switch (*(it + 1)) { + case '$': + // Eat one of the dollars. + if (!newReplaceChars.append(*it)) { + return false; + } + break; + case '&': + if (!newReplaceChars.appendSubstring(text, matchStart, + matchLimit - matchStart)) { + return false; + } + break; + case '`': + if (!newReplaceChars.appendSubstring(text, 0, matchStart)) { + return false; + } + break; + case '\'': + if (!newReplaceChars.appendSubstring(text, matchLimit, + text->length() - matchLimit)) { + return false; + } + break; + default: + // The dollar we saw was not special (no matter what its mother told + // it). + if (!newReplaceChars.append(*it)) { + return false; + } + continue; + } + ++it; // We always eat an extra char in the above switch. + } + + return true; +} + +/* + * Perform a linear-scan dollar substitution on the replacement text. + */ +static JSLinearString* InterpretDollarReplacement( + JSContext* cx, HandleString textstrArg, Handle<JSLinearString*> repstr, + uint32_t firstDollarIndex, size_t matchStart, size_t patternLength) { + Rooted<JSLinearString*> textstr(cx, textstrArg->ensureLinear(cx)); + if (!textstr) { + return nullptr; + } + + size_t matchLimit = matchStart + patternLength; + + /* + * Most probably: + * + * len(newstr) >= len(orig) - len(match) + len(replacement) + * + * Note that dollar vars _could_ make the resulting text smaller than this. + */ + JSStringBuilder newReplaceChars(cx); + if (repstr->hasTwoByteChars() && !newReplaceChars.ensureTwoByteChars()) { + return nullptr; + } + + if (!newReplaceChars.reserve(textstr->length() - patternLength + + repstr->length())) { + return nullptr; + } + + bool res; + if (repstr->hasLatin1Chars()) { + AutoCheckCannotGC nogc; + res = AppendDollarReplacement(newReplaceChars, firstDollarIndex, matchStart, + matchLimit, textstr, + repstr->latin1Chars(nogc), repstr->length()); + } else { + AutoCheckCannotGC nogc; + res = AppendDollarReplacement(newReplaceChars, firstDollarIndex, matchStart, + matchLimit, textstr, + repstr->twoByteChars(nogc), repstr->length()); + } + if (!res) { + return nullptr; + } + + return newReplaceChars.finishString(); +} + +template <typename StrChar, typename RepChar> +static bool StrFlatReplaceGlobal(JSContext* cx, JSLinearString* str, + JSLinearString* pat, JSLinearString* rep, + StringBuffer& sb) { + MOZ_ASSERT(str->length() > 0); + + AutoCheckCannotGC nogc; + const StrChar* strChars = str->chars<StrChar>(nogc); + const RepChar* repChars = rep->chars<RepChar>(nogc); + + // The pattern is empty, so we interleave the replacement string in-between + // each character. + if (!pat->length()) { + CheckedInt<uint32_t> strLength(str->length()); + CheckedInt<uint32_t> repLength(rep->length()); + CheckedInt<uint32_t> length = repLength * (strLength - 1) + strLength; + if (!length.isValid()) { + ReportAllocationOverflow(cx); + return false; + } + + if (!sb.reserve(length.value())) { + return false; + } + + for (unsigned i = 0; i < str->length() - 1; ++i, ++strChars) { + sb.infallibleAppend(*strChars); + sb.infallibleAppend(repChars, rep->length()); + } + sb.infallibleAppend(*strChars); + return true; + } + + // If it's true, we are sure that the result's length is, at least, the same + // length as |str->length()|. + if (rep->length() >= pat->length()) { + if (!sb.reserve(str->length())) { + return false; + } + } + + uint32_t start = 0; + for (;;) { + int match = StringMatch(str, pat, start); + if (match < 0) { + break; + } + if (!sb.append(strChars + start, match - start)) { + return false; + } + if (!sb.append(repChars, rep->length())) { + return false; + } + start = match + pat->length(); + } + + if (!sb.append(strChars + start, str->length() - start)) { + return false; + } + + return true; +} + +// This is identical to "str.split(pattern).join(replacement)" except that we +// do some deforestation optimization in Ion. +JSString* js::StringFlatReplaceString(JSContext* cx, HandleString string, + HandleString pattern, + HandleString replacement) { + MOZ_ASSERT(string); + MOZ_ASSERT(pattern); + MOZ_ASSERT(replacement); + + if (!string->length()) { + return string; + } + + Rooted<JSLinearString*> linearRepl(cx, replacement->ensureLinear(cx)); + if (!linearRepl) { + return nullptr; + } + + Rooted<JSLinearString*> linearPat(cx, pattern->ensureLinear(cx)); + if (!linearPat) { + return nullptr; + } + + Rooted<JSLinearString*> linearStr(cx, string->ensureLinear(cx)); + if (!linearStr) { + return nullptr; + } + + JSStringBuilder sb(cx); + if (linearStr->hasTwoByteChars()) { + if (!sb.ensureTwoByteChars()) { + return nullptr; + } + if (linearRepl->hasTwoByteChars()) { + if (!StrFlatReplaceGlobal<char16_t, char16_t>(cx, linearStr, linearPat, + linearRepl, sb)) { + return nullptr; + } + } else { + if (!StrFlatReplaceGlobal<char16_t, Latin1Char>(cx, linearStr, linearPat, + linearRepl, sb)) { + return nullptr; + } + } + } else { + if (linearRepl->hasTwoByteChars()) { + if (!sb.ensureTwoByteChars()) { + return nullptr; + } + if (!StrFlatReplaceGlobal<Latin1Char, char16_t>(cx, linearStr, linearPat, + linearRepl, sb)) { + return nullptr; + } + } else { + if (!StrFlatReplaceGlobal<Latin1Char, Latin1Char>( + cx, linearStr, linearPat, linearRepl, sb)) { + return nullptr; + } + } + } + + return sb.finishString(); +} + +JSString* js::str_replace_string_raw(JSContext* cx, HandleString string, + HandleString pattern, + HandleString replacement) { + Rooted<JSLinearString*> repl(cx, replacement->ensureLinear(cx)); + if (!repl) { + return nullptr; + } + + Rooted<JSLinearString*> pat(cx, pattern->ensureLinear(cx)); + if (!pat) { + return nullptr; + } + + size_t patternLength = pat->length(); + int32_t match; + uint32_t dollarIndex; + + { + AutoCheckCannotGC nogc; + dollarIndex = + repl->hasLatin1Chars() + ? FindDollarIndex(repl->latin1Chars(nogc), repl->length()) + : FindDollarIndex(repl->twoByteChars(nogc), repl->length()); + } + + /* + * |string| could be a rope, so we want to avoid flattening it for as + * long as possible. + */ + if (string->isRope()) { + if (!RopeMatch(cx, &string->asRope(), pat, &match)) { + return nullptr; + } + } else { + match = StringMatch(&string->asLinear(), pat, 0); + } + + if (match < 0) { + return string; + } + + if (dollarIndex != UINT32_MAX) { + repl = InterpretDollarReplacement(cx, string, repl, dollarIndex, match, + patternLength); + if (!repl) { + return nullptr; + } + } else if (string->isRope()) { + return BuildFlatRopeReplacement(cx, string, repl, match, patternLength); + } + return BuildFlatReplacement(cx, string, repl, match, patternLength); +} + +template <typename StrChar, typename RepChar> +static bool ReplaceAllInternal(const AutoCheckCannotGC& nogc, + JSLinearString* string, + JSLinearString* searchString, + JSLinearString* replaceString, + const int32_t startPosition, + JSStringBuilder& result) { + // Step 7. + const size_t stringLength = string->length(); + const size_t searchLength = searchString->length(); + const size_t replaceLength = replaceString->length(); + + MOZ_ASSERT(stringLength > 0); + MOZ_ASSERT(searchLength > 0); + MOZ_ASSERT(stringLength >= searchLength); + + // Step 12. + uint32_t endOfLastMatch = 0; + + const StrChar* strChars = string->chars<StrChar>(nogc); + const RepChar* repChars = replaceString->chars<RepChar>(nogc); + + uint32_t dollarIndex = FindDollarIndex(repChars, replaceLength); + + // If it's true, we are sure that the result's length is, at least, the same + // length as |str->length()|. + if (replaceLength >= searchLength) { + if (!result.reserve(stringLength)) { + return false; + } + } + + int32_t position = startPosition; + do { + // Step 14.c. + // Append the substring before the current match. + if (!result.append(strChars + endOfLastMatch, position - endOfLastMatch)) { + return false; + } + + // Steps 14.a-b and 14.d. + // Append the replacement. + if (dollarIndex != UINT32_MAX) { + size_t matchLimit = position + searchLength; + if (!AppendDollarReplacement(result, dollarIndex, position, matchLimit, + string, repChars, replaceLength)) { + return false; + } + } else { + if (!result.append(repChars, replaceLength)) { + return false; + } + } + + // Step 14.e. + endOfLastMatch = position + searchLength; + + // Step 11. + // Find the next match. + position = StringMatch(string, searchString, endOfLastMatch); + } while (position >= 0); + + // Step 15. + // Append the substring after the last match. + return result.append(strChars + endOfLastMatch, + stringLength - endOfLastMatch); +} + +// https://tc39.es/proposal-string-replaceall/#sec-string.prototype.replaceall +// Steps 7-16 when functionalReplace is false and searchString is not empty. +// +// The steps are quite different, for performance. Loops in steps 11 and 14 +// are fused. GetSubstitution is optimized away when possible. +template <typename StrChar, typename RepChar> +static JSString* ReplaceAll(JSContext* cx, JSLinearString* string, + JSLinearString* searchString, + JSLinearString* replaceString) { + // Step 7 moved into ReplaceAll_internal. + + // Step 8 (advanceBy is equal to searchLength when searchLength > 0). + + // Step 9 (not needed in this implementation). + + // Step 10. + // Find the first match. + int32_t position = StringMatch(string, searchString, 0); + + // Nothing to replace, so return early. + if (position < 0) { + return string; + } + + // Steps 11, 12 moved into ReplaceAll_internal. + + // Step 13. + JSStringBuilder result(cx); + if constexpr (std::is_same_v<StrChar, char16_t> || + std::is_same_v<RepChar, char16_t>) { + if (!result.ensureTwoByteChars()) { + return nullptr; + } + } + + bool internalFailure = false; + { + AutoCheckCannotGC nogc; + internalFailure = !ReplaceAllInternal<StrChar, RepChar>( + nogc, string, searchString, replaceString, position, result); + } + if (internalFailure) { + return nullptr; + } + + // Step 16. + return result.finishString(); +} + +template <typename StrChar, typename RepChar> +static bool ReplaceAllInterleaveInternal(const AutoCheckCannotGC& nogc, + JSContext* cx, JSLinearString* string, + JSLinearString* replaceString, + JSStringBuilder& result) { + // Step 7. + const size_t stringLength = string->length(); + const size_t replaceLength = replaceString->length(); + + const StrChar* strChars = string->chars<StrChar>(nogc); + const RepChar* repChars = replaceString->chars<RepChar>(nogc); + + uint32_t dollarIndex = FindDollarIndex(repChars, replaceLength); + + if (dollarIndex != UINT32_MAX) { + if (!result.reserve(stringLength)) { + return false; + } + } else { + // Compute the exact result length when no substitutions take place. + CheckedInt<uint32_t> strLength(stringLength); + CheckedInt<uint32_t> repLength(replaceLength); + CheckedInt<uint32_t> length = strLength + (strLength + 1) * repLength; + if (!length.isValid()) { + ReportAllocationOverflow(cx); + return false; + } + + if (!result.reserve(length.value())) { + return false; + } + } + + auto appendReplacement = [&](size_t match) { + if (dollarIndex != UINT32_MAX) { + return AppendDollarReplacement(result, dollarIndex, match, match, string, + repChars, replaceLength); + } + return result.append(repChars, replaceLength); + }; + + for (size_t index = 0; index < stringLength; index++) { + // Steps 11, 14.a-b and 14.d. + // The empty string matches before each character. + if (!appendReplacement(index)) { + return false; + } + + // Step 14.c. + if (!result.append(strChars[index])) { + return false; + } + } + + // Steps 11, 14.a-b and 14.d. + // The empty string also matches at the end of the string. + return appendReplacement(stringLength); + + // Step 15 (not applicable when searchString is the empty string). +} + +// https://tc39.es/proposal-string-replaceall/#sec-string.prototype.replaceall +// Steps 7-16 when functionalReplace is false and searchString is the empty +// string. +// +// The steps are quite different, for performance. Loops in steps 11 and 14 +// are fused. GetSubstitution is optimized away when possible. +template <typename StrChar, typename RepChar> +static JSString* ReplaceAllInterleave(JSContext* cx, JSLinearString* string, + JSLinearString* replaceString) { + // Step 7 moved into ReplaceAllInterleavedInternal. + + // Step 8 (advanceBy is 1 when searchString is the empty string). + + // Steps 9-12 (trivial when searchString is the empty string). + + // Step 13. + JSStringBuilder result(cx); + if constexpr (std::is_same_v<StrChar, char16_t> || + std::is_same_v<RepChar, char16_t>) { + if (!result.ensureTwoByteChars()) { + return nullptr; + } + } + + bool internalFailure = false; + { + AutoCheckCannotGC nogc; + internalFailure = !ReplaceAllInterleaveInternal<StrChar, RepChar>( + nogc, cx, string, replaceString, result); + } + if (internalFailure) { + return nullptr; + } + + // Step 16. + return result.finishString(); +} + +// String.prototype.replaceAll (Stage 3 proposal) +// https://tc39.es/proposal-string-replaceall/ +// +// String.prototype.replaceAll ( searchValue, replaceValue ) +// +// Steps 7-16 when functionalReplace is false. +JSString* js::str_replaceAll_string_raw(JSContext* cx, HandleString string, + HandleString searchString, + HandleString replaceString) { + const size_t stringLength = string->length(); + const size_t searchLength = searchString->length(); + + // Directly return when we're guaranteed to find no match. + if (searchLength > stringLength) { + return string; + } + + Rooted<JSLinearString*> str(cx, string->ensureLinear(cx)); + if (!str) { + return nullptr; + } + + Rooted<JSLinearString*> repl(cx, replaceString->ensureLinear(cx)); + if (!repl) { + return nullptr; + } + + Rooted<JSLinearString*> search(cx, searchString->ensureLinear(cx)); + if (!search) { + return nullptr; + } + + // The pattern is empty, so we interleave the replacement string in-between + // each character. + if (searchLength == 0) { + if (str->hasTwoByteChars()) { + if (repl->hasTwoByteChars()) { + return ReplaceAllInterleave<char16_t, char16_t>(cx, str, repl); + } + return ReplaceAllInterleave<char16_t, Latin1Char>(cx, str, repl); + } + if (repl->hasTwoByteChars()) { + return ReplaceAllInterleave<Latin1Char, char16_t>(cx, str, repl); + } + return ReplaceAllInterleave<Latin1Char, Latin1Char>(cx, str, repl); + } + + MOZ_ASSERT(stringLength > 0); + + if (str->hasTwoByteChars()) { + if (repl->hasTwoByteChars()) { + return ReplaceAll<char16_t, char16_t>(cx, str, search, repl); + } + return ReplaceAll<char16_t, Latin1Char>(cx, str, search, repl); + } + if (repl->hasTwoByteChars()) { + return ReplaceAll<Latin1Char, char16_t>(cx, str, search, repl); + } + return ReplaceAll<Latin1Char, Latin1Char>(cx, str, search, repl); +} + +static ArrayObject* SingleElementStringArray(JSContext* cx, + Handle<JSLinearString*> str) { + ArrayObject* array = NewDenseFullyAllocatedArray(cx, 1); + if (!array) { + return nullptr; + } + array->setDenseInitializedLength(1); + array->initDenseElement(0, StringValue(str)); + return array; +} + +// ES 2016 draft Mar 25, 2016 21.1.3.17 steps 4, 8, 12-18. +static ArrayObject* SplitHelper(JSContext* cx, Handle<JSLinearString*> str, + uint32_t limit, Handle<JSLinearString*> sep) { + size_t strLength = str->length(); + size_t sepLength = sep->length(); + MOZ_ASSERT(sepLength != 0); + + // Step 12. + if (strLength == 0) { + // Step 12.a. + int match = StringMatch(str, sep, 0); + + // Step 12.b. + if (match != -1) { + return NewDenseEmptyArray(cx); + } + + // Steps 12.c-e. + return SingleElementStringArray(cx, str); + } + + // Step 3 (reordered). + RootedValueVector splits(cx); + + // Step 8 (reordered). + size_t lastEndIndex = 0; + + // Step 13. + size_t index = 0; + + // Step 14. + while (index != strLength) { + // Step 14.a. + int match = StringMatch(str, sep, index); + + // Step 14.b. + // + // Our match algorithm differs from the spec in that it returns the + // next index at which a match happens. If no match happens we're + // done. + // + // But what if the match is at the end of the string (and the string is + // not empty)? Per 14.c.i this shouldn't be a match, so we have to + // specially exclude it. Thus this case should hold: + // + // var a = "abc".split(/\b/); + // assertEq(a.length, 1); + // assertEq(a[0], "abc"); + if (match == -1) { + break; + } + + // Step 14.c. + size_t endIndex = match + sepLength; + + // Step 14.c.i. + if (endIndex == lastEndIndex) { + index++; + continue; + } + + // Step 14.c.ii. + MOZ_ASSERT(lastEndIndex < endIndex); + MOZ_ASSERT(sepLength <= strLength); + MOZ_ASSERT(lastEndIndex + sepLength <= endIndex); + + // Step 14.c.ii.1. + size_t subLength = size_t(endIndex - sepLength - lastEndIndex); + JSString* sub = NewDependentString(cx, str, lastEndIndex, subLength); + + // Steps 14.c.ii.2-4. + if (!sub || !splits.append(StringValue(sub))) { + return nullptr; + } + + // Step 14.c.ii.5. + if (splits.length() == limit) { + return NewDenseCopiedArray(cx, splits.length(), splits.begin()); + } + + // Step 14.c.ii.6. + index = endIndex; + + // Step 14.c.ii.7. + lastEndIndex = index; + } + + // Step 15. + JSString* sub = + NewDependentString(cx, str, lastEndIndex, strLength - lastEndIndex); + + // Steps 16-17. + if (!sub || !splits.append(StringValue(sub))) { + return nullptr; + } + + // Step 18. + return NewDenseCopiedArray(cx, splits.length(), splits.begin()); +} + +// Fast-path for splitting a string into a character array via split(""). +static ArrayObject* CharSplitHelper(JSContext* cx, Handle<JSLinearString*> str, + uint32_t limit) { + size_t strLength = str->length(); + if (strLength == 0) { + return NewDenseEmptyArray(cx); + } + + js::StaticStrings& staticStrings = cx->staticStrings(); + uint32_t resultlen = (limit < strLength ? limit : strLength); + MOZ_ASSERT(limit > 0 && resultlen > 0, + "Neither limit nor strLength is zero, so resultlen is greater " + "than zero."); + + Rooted<ArrayObject*> splits(cx, NewDenseFullyAllocatedArray(cx, resultlen)); + if (!splits) { + return nullptr; + } + + if (str->hasLatin1Chars()) { + splits->setDenseInitializedLength(resultlen); + + JS::AutoCheckCannotGC nogc; + const Latin1Char* latin1Chars = str->latin1Chars(nogc); + for (size_t i = 0; i < resultlen; ++i) { + Latin1Char c = latin1Chars[i]; + MOZ_ASSERT(staticStrings.hasUnit(c)); + splits->initDenseElement(i, StringValue(staticStrings.getUnit(c))); + } + } else { + splits->ensureDenseInitializedLength(0, resultlen); + + for (size_t i = 0; i < resultlen; ++i) { + JSString* sub = staticStrings.getUnitStringForElement(cx, str, i); + if (!sub) { + return nullptr; + } + splits->initDenseElement(i, StringValue(sub)); + } + } + + return splits; +} + +template <typename TextChar> +static MOZ_ALWAYS_INLINE ArrayObject* SplitSingleCharHelper( + JSContext* cx, Handle<JSLinearString*> str, const TextChar* text, + uint32_t textLen, char16_t patCh) { + // Count the number of occurrences of patCh within text. + uint32_t count = 0; + for (size_t index = 0; index < textLen; index++) { + if (static_cast<char16_t>(text[index]) == patCh) { + count++; + } + } + + // Handle zero-occurrence case - return input string in an array. + if (count == 0) { + return SingleElementStringArray(cx, str); + } + + // Create the result array for the substring values. + Rooted<ArrayObject*> splits(cx, NewDenseFullyAllocatedArray(cx, count + 1)); + if (!splits) { + return nullptr; + } + splits->ensureDenseInitializedLength(0, count + 1); + + // Add substrings. + uint32_t splitsIndex = 0; + size_t lastEndIndex = 0; + for (size_t index = 0; index < textLen; index++) { + if (static_cast<char16_t>(text[index]) == patCh) { + size_t subLength = size_t(index - lastEndIndex); + JSString* sub = NewDependentString(cx, str, lastEndIndex, subLength); + if (!sub) { + return nullptr; + } + splits->initDenseElement(splitsIndex++, StringValue(sub)); + lastEndIndex = index + 1; + } + } + + // Add substring for tail of string (after last match). + JSString* sub = + NewDependentString(cx, str, lastEndIndex, textLen - lastEndIndex); + if (!sub) { + return nullptr; + } + splits->initDenseElement(splitsIndex++, StringValue(sub)); + + return splits; +} + +// ES 2016 draft Mar 25, 2016 21.1.3.17 steps 4, 8, 12-18. +static ArrayObject* SplitSingleCharHelper(JSContext* cx, + Handle<JSLinearString*> str, + char16_t ch) { + // Step 12. + size_t strLength = str->length(); + + AutoStableStringChars linearChars(cx); + if (!linearChars.init(cx, str)) { + return nullptr; + } + + if (linearChars.isLatin1()) { + return SplitSingleCharHelper(cx, str, linearChars.latin1Chars(), strLength, + ch); + } + + return SplitSingleCharHelper(cx, str, linearChars.twoByteChars(), strLength, + ch); +} + +// ES 2016 draft Mar 25, 2016 21.1.3.17 steps 4, 8, 12-18. +ArrayObject* js::StringSplitString(JSContext* cx, HandleString str, + HandleString sep, uint32_t limit) { + MOZ_ASSERT(limit > 0, "Only called for strictly positive limit."); + + Rooted<JSLinearString*> linearStr(cx, str->ensureLinear(cx)); + if (!linearStr) { + return nullptr; + } + + Rooted<JSLinearString*> linearSep(cx, sep->ensureLinear(cx)); + if (!linearSep) { + return nullptr; + } + + if (linearSep->length() == 0) { + return CharSplitHelper(cx, linearStr, limit); + } + + if (linearSep->length() == 1 && limit >= static_cast<uint32_t>(INT32_MAX)) { + char16_t ch = linearSep->latin1OrTwoByteChar(0); + return SplitSingleCharHelper(cx, linearStr, ch); + } + + return SplitHelper(cx, linearStr, limit, linearSep); +} + +static const JSFunctionSpec string_methods[] = { + JS_FN(js_toSource_str, str_toSource, 0, 0), + + /* Java-like methods. */ + JS_INLINABLE_FN(js_toString_str, str_toString, 0, 0, StringToString), + JS_INLINABLE_FN(js_valueOf_str, str_toString, 0, 0, StringValueOf), + JS_INLINABLE_FN("toLowerCase", str_toLowerCase, 0, 0, StringToLowerCase), + JS_INLINABLE_FN("toUpperCase", str_toUpperCase, 0, 0, StringToUpperCase), + JS_INLINABLE_FN("charAt", str_charAt, 1, 0, StringCharAt), + JS_INLINABLE_FN("charCodeAt", str_charCodeAt, 1, 0, StringCharCodeAt), + JS_SELF_HOSTED_FN("substring", "String_substring", 2, 0), + JS_SELF_HOSTED_FN("padStart", "String_pad_start", 2, 0), + JS_SELF_HOSTED_FN("padEnd", "String_pad_end", 2, 0), + JS_SELF_HOSTED_FN("codePointAt", "String_codePointAt", 1, 0), + JS_FN("includes", str_includes, 1, 0), + JS_INLINABLE_FN("indexOf", str_indexOf, 1, 0, StringIndexOf), + JS_FN("lastIndexOf", str_lastIndexOf, 1, 0), + JS_INLINABLE_FN("startsWith", str_startsWith, 1, 0, StringStartsWith), + JS_INLINABLE_FN("endsWith", str_endsWith, 1, 0, StringEndsWith), + JS_FN("trim", str_trim, 0, 0), + JS_FN("trimStart", str_trimStart, 0, 0), + JS_FN("trimEnd", str_trimEnd, 0, 0), +#if JS_HAS_INTL_API + JS_SELF_HOSTED_FN("toLocaleLowerCase", "String_toLocaleLowerCase", 0, 0), + JS_SELF_HOSTED_FN("toLocaleUpperCase", "String_toLocaleUpperCase", 0, 0), + JS_SELF_HOSTED_FN("localeCompare", "String_localeCompare", 1, 0), +#else + JS_FN("toLocaleLowerCase", str_toLocaleLowerCase, 0, 0), + JS_FN("toLocaleUpperCase", str_toLocaleUpperCase, 0, 0), + JS_FN("localeCompare", str_localeCompare, 1, 0), +#endif + JS_SELF_HOSTED_FN("repeat", "String_repeat", 1, 0), +#if JS_HAS_INTL_API + JS_FN("normalize", str_normalize, 0, 0), +#endif +#ifdef NIGHTLY_BUILD + JS_FN("isWellFormed", str_isWellFormed, 0, 0), + JS_FN("toWellFormed", str_toWellFormed, 0, 0), +#endif + + /* Perl-ish methods (search is actually Python-esque). */ + JS_SELF_HOSTED_FN("match", "String_match", 1, 0), + JS_SELF_HOSTED_FN("matchAll", "String_matchAll", 1, 0), + JS_SELF_HOSTED_FN("search", "String_search", 1, 0), + JS_SELF_HOSTED_FN("replace", "String_replace", 2, 0), + JS_SELF_HOSTED_FN("replaceAll", "String_replaceAll", 2, 0), + JS_SELF_HOSTED_FN("split", "String_split", 2, 0), + JS_SELF_HOSTED_FN("substr", "String_substr", 2, 0), + + /* Python-esque sequence methods. */ + JS_SELF_HOSTED_FN("concat", "String_concat", 1, 0), + JS_SELF_HOSTED_FN("slice", "String_slice", 2, 0), + + JS_SELF_HOSTED_FN("at", "String_at", 1, 0), + + /* HTML string methods. */ + JS_SELF_HOSTED_FN("bold", "String_bold", 0, 0), + JS_SELF_HOSTED_FN("italics", "String_italics", 0, 0), + JS_SELF_HOSTED_FN("fixed", "String_fixed", 0, 0), + JS_SELF_HOSTED_FN("strike", "String_strike", 0, 0), + JS_SELF_HOSTED_FN("small", "String_small", 0, 0), + JS_SELF_HOSTED_FN("big", "String_big", 0, 0), + JS_SELF_HOSTED_FN("blink", "String_blink", 0, 0), + JS_SELF_HOSTED_FN("sup", "String_sup", 0, 0), + JS_SELF_HOSTED_FN("sub", "String_sub", 0, 0), + JS_SELF_HOSTED_FN("anchor", "String_anchor", 1, 0), + JS_SELF_HOSTED_FN("link", "String_link", 1, 0), + JS_SELF_HOSTED_FN("fontcolor", "String_fontcolor", 1, 0), + JS_SELF_HOSTED_FN("fontsize", "String_fontsize", 1, 0), + + JS_SELF_HOSTED_SYM_FN(iterator, "String_iterator", 0, 0), + JS_FS_END, +}; + +// ES6 rev 27 (2014 Aug 24) 21.1.1 +bool js::StringConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString str(cx); + if (args.length() > 0) { + if (!args.isConstructing() && args[0].isSymbol()) { + return js::SymbolDescriptiveString(cx, args[0].toSymbol(), args.rval()); + } + + str = ToString<CanGC>(cx, args[0]); + if (!str) { + return false; + } + } else { + str = cx->runtime()->emptyString; + } + + if (args.isConstructing()) { + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_String, &proto)) { + return false; + } + + StringObject* strobj = StringObject::create(cx, str, proto); + if (!strobj) { + return false; + } + args.rval().setObject(*strobj); + return true; + } + + args.rval().setString(str); + return true; +} + +bool js::str_fromCharCode(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + MOZ_ASSERT(args.length() <= ARGS_LENGTH_MAX); + + // Optimize the single-char case. + if (args.length() == 1) { + return str_fromCharCode_one_arg(cx, args[0], args.rval()); + } + + // Optimize the case where the result will definitely fit in an inline + // string (thin or fat) and so we don't need to malloc the chars. (We could + // cover some cases where args.length() goes up to + // JSFatInlineString::MAX_LENGTH_LATIN1 if we also checked if the chars are + // all Latin-1, but it doesn't seem worth the effort.) + InlineCharBuffer<char16_t> chars; + if (!chars.maybeAlloc(cx, args.length())) { + return false; + } + + char16_t* rawChars = chars.get(); + for (unsigned i = 0; i < args.length(); i++) { + uint16_t code; + if (!ToUint16(cx, args[i], &code)) { + return false; + } + + rawChars[i] = char16_t(code); + } + + JSString* str = chars.toString(cx, args.length()); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static inline bool CodeUnitToString(JSContext* cx, uint16_t ucode, + MutableHandleValue rval) { + if (StaticStrings::hasUnit(ucode)) { + rval.setString(cx->staticStrings().getUnit(ucode)); + return true; + } + + char16_t c = char16_t(ucode); + JSString* str = NewStringCopyNDontDeflate<CanGC>(cx, &c, 1); + if (!str) { + return false; + } + + rval.setString(str); + return true; +} + +bool js::str_fromCharCode_one_arg(JSContext* cx, HandleValue code, + MutableHandleValue rval) { + uint16_t ucode; + + if (!ToUint16(cx, code, &ucode)) { + return false; + } + + return CodeUnitToString(cx, ucode, rval); +} + +static MOZ_ALWAYS_INLINE bool ToCodePoint(JSContext* cx, HandleValue code, + char32_t* codePoint) { + // String.fromCodePoint, Steps 5.a-b. + + // Fast path for the common case - the input is already an int32. + if (code.isInt32()) { + int32_t nextCP = code.toInt32(); + if (nextCP >= 0 && nextCP <= int32_t(unicode::NonBMPMax)) { + *codePoint = char32_t(nextCP); + return true; + } + } + + double nextCP; + if (!ToNumber(cx, code, &nextCP)) { + return false; + } + + // String.fromCodePoint, Steps 5.c-d. + if (JS::ToInteger(nextCP) != nextCP || nextCP < 0 || + nextCP > unicode::NonBMPMax) { + ToCStringBuf cbuf; + const char* numStr = NumberToCString(&cbuf, nextCP); + MOZ_ASSERT(numStr); + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_A_CODEPOINT, numStr); + return false; + } + + *codePoint = char32_t(nextCP); + return true; +} + +bool js::str_fromCodePoint_one_arg(JSContext* cx, HandleValue code, + MutableHandleValue rval) { + // Steps 1-4 (omitted). + + // Steps 5.a-d. + char32_t codePoint; + if (!ToCodePoint(cx, code, &codePoint)) { + return false; + } + + // Steps 5.e, 6. + if (!unicode::IsSupplementary(codePoint)) { + return CodeUnitToString(cx, uint16_t(codePoint), rval); + } + + char16_t chars[] = {unicode::LeadSurrogate(codePoint), + unicode::TrailSurrogate(codePoint)}; + JSString* str = NewStringCopyNDontDeflate<CanGC>(cx, chars, 2); + if (!str) { + return false; + } + + rval.setString(str); + return true; +} + +static bool str_fromCodePoint_few_args(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(args.length() <= JSFatInlineString::MAX_LENGTH_TWO_BYTE / 2); + + // Steps 1-2 (omitted). + + // Step 3. + char16_t elements[JSFatInlineString::MAX_LENGTH_TWO_BYTE]; + + // Steps 4-5. + unsigned length = 0; + for (unsigned nextIndex = 0; nextIndex < args.length(); nextIndex++) { + // Steps 5.a-d. + char32_t codePoint; + if (!ToCodePoint(cx, args[nextIndex], &codePoint)) { + return false; + } + + // Step 5.e. + unicode::UTF16Encode(codePoint, elements, &length); + } + + // Step 6. + JSString* str = NewStringCopyN<CanGC>(cx, elements, length); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +// ES2017 draft rev 40edb3a95a475c1b251141ac681b8793129d9a6d +// 21.1.2.2 String.fromCodePoint(...codePoints) +bool js::str_fromCodePoint(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Optimize the single code-point case. + if (args.length() == 1) { + return str_fromCodePoint_one_arg(cx, args[0], args.rval()); + } + + // Optimize the case where the result will definitely fit in an inline + // string (thin or fat) and so we don't need to malloc the chars. (We could + // cover some cases where |args.length()| goes up to + // JSFatInlineString::MAX_LENGTH_LATIN1 / 2 if we also checked if the chars + // are all Latin-1, but it doesn't seem worth the effort.) + if (args.length() <= JSFatInlineString::MAX_LENGTH_TWO_BYTE / 2) { + return str_fromCodePoint_few_args(cx, args); + } + + // Steps 1-2 (omitted). + + // Step 3. + static_assert( + ARGS_LENGTH_MAX < std::numeric_limits<decltype(args.length())>::max() / 2, + "|args.length() * 2| does not overflow"); + auto elements = cx->make_pod_arena_array<char16_t>(js::StringBufferArena, + args.length() * 2); + if (!elements) { + return false; + } + + // Steps 4-5. + unsigned length = 0; + for (unsigned nextIndex = 0; nextIndex < args.length(); nextIndex++) { + // Steps 5.a-d. + char32_t codePoint; + if (!ToCodePoint(cx, args[nextIndex], &codePoint)) { + return false; + } + + // Step 5.e. + unicode::UTF16Encode(codePoint, elements.get(), &length); + } + + // Step 6. + JSString* str = NewString<CanGC>(cx, std::move(elements), length); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static const JSFunctionSpec string_static_methods[] = { + JS_INLINABLE_FN("fromCharCode", js::str_fromCharCode, 1, 0, + StringFromCharCode), + JS_INLINABLE_FN("fromCodePoint", js::str_fromCodePoint, 1, 0, + StringFromCodePoint), + + JS_SELF_HOSTED_FN("raw", "String_static_raw", 1, 0), JS_FS_END}; + +/* static */ +SharedShape* StringObject::assignInitialShape(JSContext* cx, + Handle<StringObject*> obj) { + MOZ_ASSERT(obj->empty()); + + if (!NativeObject::addPropertyInReservedSlot(cx, obj, cx->names().length, + LENGTH_SLOT, {})) { + return nullptr; + } + + return obj->sharedShape(); +} + +JSObject* StringObject::createPrototype(JSContext* cx, JSProtoKey key) { + Rooted<JSString*> empty(cx, cx->runtime()->emptyString); + Rooted<StringObject*> proto( + cx, GlobalObject::createBlankPrototype<StringObject>(cx, cx->global())); + if (!proto) { + return nullptr; + } + if (!StringObject::init(cx, proto, empty)) { + return nullptr; + } + return proto; +} + +static bool StringClassFinish(JSContext* cx, HandleObject ctor, + HandleObject proto) { + Handle<NativeObject*> nativeProto = proto.as<NativeObject>(); + + // Create "trimLeft" as an alias for "trimStart". + RootedValue trimFn(cx); + RootedId trimId(cx, NameToId(cx->names().trimStart)); + RootedId trimAliasId(cx, NameToId(cx->names().trimLeft)); + if (!NativeGetProperty(cx, nativeProto, trimId, &trimFn) || + !NativeDefineDataProperty(cx, nativeProto, trimAliasId, trimFn, 0)) { + return false; + } + + // Create "trimRight" as an alias for "trimEnd". + trimId = NameToId(cx->names().trimEnd); + trimAliasId = NameToId(cx->names().trimRight); + if (!NativeGetProperty(cx, nativeProto, trimId, &trimFn) || + !NativeDefineDataProperty(cx, nativeProto, trimAliasId, trimFn, 0)) { + return false; + } + + /* + * Define escape/unescape, the URI encode/decode functions, and maybe + * uneval on the global object. + */ + if (!JS_DefineFunctions(cx, cx->global(), string_functions)) { + return false; + } + + return true; +} + +const ClassSpec StringObject::classSpec_ = { + GenericCreateConstructor<StringConstructor, 1, gc::AllocKind::FUNCTION, + &jit::JitInfo_String>, + StringObject::createPrototype, + string_static_methods, + nullptr, + string_methods, + nullptr, + StringClassFinish}; + +#define ____ false + +/* + * Uri reserved chars + #: + * - 35: # + * - 36: $ + * - 38: & + * - 43: + + * - 44: , + * - 47: / + * - 58: : + * - 59: ; + * - 61: = + * - 63: ? + * - 64: @ + */ +static const bool js_isUriReservedPlusPound[] = { + // clang-format off +/* 0 1 2 3 4 5 6 7 8 9 */ +/* 0 */ ____, ____, ____, ____, ____, ____, ____, ____, ____, ____, +/* 1 */ ____, ____, ____, ____, ____, ____, ____, ____, ____, ____, +/* 2 */ ____, ____, ____, ____, ____, ____, ____, ____, ____, ____, +/* 3 */ ____, ____, ____, ____, ____, true, true, ____, true, ____, +/* 4 */ ____, ____, ____, true, true, ____, ____, true, ____, ____, +/* 5 */ ____, ____, ____, ____, ____, ____, ____, ____, true, true, +/* 6 */ ____, true, ____, true, true, ____, ____, ____, ____, ____, +/* 7 */ ____, ____, ____, ____, ____, ____, ____, ____, ____, ____, +/* 8 */ ____, ____, ____, ____, ____, ____, ____, ____, ____, ____, +/* 9 */ ____, ____, ____, ____, ____, ____, ____, ____, ____, ____, +/* 10 */ ____, ____, ____, ____, ____, ____, ____, ____, ____, ____, +/* 11 */ ____, ____, ____, ____, ____, ____, ____, ____, ____, ____, +/* 12 */ ____, ____, ____, ____, ____, ____, ____, ____ + // clang-format on +}; + +/* + * Uri unescaped chars: + * - 33: ! + * - 39: ' + * - 40: ( + * - 41: ) + * - 42: * + * - 45: - + * - 46: . + * - 48..57: 0-9 + * - 65..90: A-Z + * - 95: _ + * - 97..122: a-z + * - 126: ~ + */ +static const bool js_isUriUnescaped[] = { + // clang-format off +/* 0 1 2 3 4 5 6 7 8 9 */ +/* 0 */ ____, ____, ____, ____, ____, ____, ____, ____, ____, ____, +/* 1 */ ____, ____, ____, ____, ____, ____, ____, ____, ____, ____, +/* 2 */ ____, ____, ____, ____, ____, ____, ____, ____, ____, ____, +/* 3 */ ____, ____, ____, true, ____, ____, ____, ____, ____, true, +/* 4 */ true, true, true, ____, ____, true, true, ____, true, true, +/* 5 */ true, true, true, true, true, true, true, true, ____, ____, +/* 6 */ ____, ____, ____, ____, ____, true, true, true, true, true, +/* 7 */ true, true, true, true, true, true, true, true, true, true, +/* 8 */ true, true, true, true, true, true, true, true, true, true, +/* 9 */ true, ____, ____, ____, ____, true, ____, true, true, true, +/* 10 */ true, true, true, true, true, true, true, true, true, true, +/* 11 */ true, true, true, true, true, true, true, true, true, true, +/* 12 */ true, true, true, ____, ____, ____, true, ____ + // clang-format on +}; + +#undef ____ + +static inline bool TransferBufferToString(JSStringBuilder& sb, JSString* str, + MutableHandleValue rval) { + if (!sb.empty()) { + str = sb.finishString(); + if (!str) { + return false; + } + } + rval.setString(str); + return true; +} + +/* + * ECMA 3, 15.1.3 URI Handling Function Properties + * + * The following are implementations of the algorithms + * given in the ECMA specification for the hidden functions + * 'Encode' and 'Decode'. + */ +enum EncodeResult { Encode_Failure, Encode_BadUri, Encode_Success }; + +// Bug 1403318: GCC sometimes inlines this Encode function rather than the +// caller Encode function. Annotate both functions with MOZ_NEVER_INLINE resp. +// MOZ_ALWAYS_INLINE to ensure we get the desired inlining behavior. +template <typename CharT> +static MOZ_NEVER_INLINE EncodeResult Encode(StringBuffer& sb, + const CharT* chars, size_t length, + const bool* unescapedSet) { + Latin1Char hexBuf[3]; + hexBuf[0] = '%'; + + auto appendEncoded = [&sb, &hexBuf](Latin1Char c) { + static const char HexDigits[] = "0123456789ABCDEF"; /* NB: uppercase */ + + hexBuf[1] = HexDigits[c >> 4]; + hexBuf[2] = HexDigits[c & 0xf]; + return sb.append(hexBuf, 3); + }; + + auto appendRange = [&sb, chars, length](size_t start, size_t end) { + MOZ_ASSERT(start <= end); + + if (start < end) { + if (start == 0) { + if (!sb.reserve(length)) { + return false; + } + } + return sb.append(chars + start, chars + end); + } + return true; + }; + + size_t startAppend = 0; + for (size_t k = 0; k < length; k++) { + CharT c = chars[k]; + if (c < 128 && + (js_isUriUnescaped[c] || (unescapedSet && unescapedSet[c]))) { + continue; + } else { + if (!appendRange(startAppend, k)) { + return Encode_Failure; + } + + if constexpr (std::is_same_v<CharT, Latin1Char>) { + if (c < 0x80) { + if (!appendEncoded(c)) { + return Encode_Failure; + } + } else { + if (!appendEncoded(0xC0 | (c >> 6)) || + !appendEncoded(0x80 | (c & 0x3F))) { + return Encode_Failure; + } + } + } else { + if (unicode::IsTrailSurrogate(c)) { + return Encode_BadUri; + } + + char32_t v; + if (!unicode::IsLeadSurrogate(c)) { + v = c; + } else { + k++; + if (k == length) { + return Encode_BadUri; + } + + char16_t c2 = chars[k]; + if (!unicode::IsTrailSurrogate(c2)) { + return Encode_BadUri; + } + + v = unicode::UTF16Decode(c, c2); + } + + uint8_t utf8buf[4]; + size_t L = OneUcs4ToUtf8Char(utf8buf, v); + for (size_t j = 0; j < L; j++) { + if (!appendEncoded(utf8buf[j])) { + return Encode_Failure; + } + } + } + + startAppend = k + 1; + } + } + + if (startAppend > 0) { + if (!appendRange(startAppend, length)) { + return Encode_Failure; + } + } + + return Encode_Success; +} + +static MOZ_ALWAYS_INLINE bool Encode(JSContext* cx, Handle<JSLinearString*> str, + const bool* unescapedSet, + MutableHandleValue rval) { + size_t length = str->length(); + if (length == 0) { + rval.setString(cx->runtime()->emptyString); + return true; + } + + JSStringBuilder sb(cx); + + EncodeResult res; + if (str->hasLatin1Chars()) { + AutoCheckCannotGC nogc; + res = Encode(sb, str->latin1Chars(nogc), str->length(), unescapedSet); + } else { + AutoCheckCannotGC nogc; + res = Encode(sb, str->twoByteChars(nogc), str->length(), unescapedSet); + } + + if (res == Encode_Failure) { + return false; + } + + if (res == Encode_BadUri) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_URI); + return false; + } + + MOZ_ASSERT(res == Encode_Success); + return TransferBufferToString(sb, str, rval); +} + +enum DecodeResult { Decode_Failure, Decode_BadUri, Decode_Success }; + +template <typename CharT> +static DecodeResult Decode(StringBuffer& sb, const CharT* chars, size_t length, + const bool* reservedSet) { + auto appendRange = [&sb, chars](size_t start, size_t end) { + MOZ_ASSERT(start <= end); + + if (start < end) { + return sb.append(chars + start, chars + end); + } + return true; + }; + + size_t startAppend = 0; + for (size_t k = 0; k < length; k++) { + CharT c = chars[k]; + if (c == '%') { + size_t start = k; + if ((k + 2) >= length) { + return Decode_BadUri; + } + + if (!IsAsciiHexDigit(chars[k + 1]) || !IsAsciiHexDigit(chars[k + 2])) { + return Decode_BadUri; + } + + uint32_t B = AsciiAlphanumericToNumber(chars[k + 1]) * 16 + + AsciiAlphanumericToNumber(chars[k + 2]); + k += 2; + if (B < 128) { + Latin1Char ch = Latin1Char(B); + if (reservedSet && reservedSet[ch]) { + continue; + } + + if (!appendRange(startAppend, start)) { + return Decode_Failure; + } + if (!sb.append(ch)) { + return Decode_Failure; + } + } else { + int n = 1; + while (B & (0x80 >> n)) { + n++; + } + + if (n == 1 || n > 4) { + return Decode_BadUri; + } + + uint8_t octets[4]; + octets[0] = (uint8_t)B; + if (k + 3 * (n - 1) >= length) { + return Decode_BadUri; + } + + for (int j = 1; j < n; j++) { + k++; + if (chars[k] != '%') { + return Decode_BadUri; + } + + if (!IsAsciiHexDigit(chars[k + 1]) || + !IsAsciiHexDigit(chars[k + 2])) { + return Decode_BadUri; + } + + B = AsciiAlphanumericToNumber(chars[k + 1]) * 16 + + AsciiAlphanumericToNumber(chars[k + 2]); + if ((B & 0xC0) != 0x80) { + return Decode_BadUri; + } + + k += 2; + octets[j] = char(B); + } + + if (!appendRange(startAppend, start)) { + return Decode_Failure; + } + + char32_t v = JS::Utf8ToOneUcs4Char(octets, n); + MOZ_ASSERT(v >= 128); + if (v >= unicode::NonBMPMin) { + if (v > unicode::NonBMPMax) { + return Decode_BadUri; + } + + if (!sb.append(unicode::LeadSurrogate(v))) { + return Decode_Failure; + } + if (!sb.append(unicode::TrailSurrogate(v))) { + return Decode_Failure; + } + } else { + if (!sb.append(char16_t(v))) { + return Decode_Failure; + } + } + } + + startAppend = k + 1; + } + } + + if (startAppend > 0) { + if (!appendRange(startAppend, length)) { + return Decode_Failure; + } + } + + return Decode_Success; +} + +static bool Decode(JSContext* cx, Handle<JSLinearString*> str, + const bool* reservedSet, MutableHandleValue rval) { + size_t length = str->length(); + if (length == 0) { + rval.setString(cx->runtime()->emptyString); + return true; + } + + JSStringBuilder sb(cx); + + DecodeResult res; + if (str->hasLatin1Chars()) { + AutoCheckCannotGC nogc; + res = Decode(sb, str->latin1Chars(nogc), str->length(), reservedSet); + } else { + AutoCheckCannotGC nogc; + res = Decode(sb, str->twoByteChars(nogc), str->length(), reservedSet); + } + + if (res == Decode_Failure) { + return false; + } + + if (res == Decode_BadUri) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_URI); + return false; + } + + MOZ_ASSERT(res == Decode_Success); + return TransferBufferToString(sb, str, rval); +} + +static bool str_decodeURI(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "decodeURI"); + CallArgs args = CallArgsFromVp(argc, vp); + Rooted<JSLinearString*> str(cx, ArgToLinearString(cx, args, 0)); + if (!str) { + return false; + } + + return Decode(cx, str, js_isUriReservedPlusPound, args.rval()); +} + +static bool str_decodeURI_Component(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "decodeURIComponent"); + CallArgs args = CallArgsFromVp(argc, vp); + Rooted<JSLinearString*> str(cx, ArgToLinearString(cx, args, 0)); + if (!str) { + return false; + } + + return Decode(cx, str, nullptr, args.rval()); +} + +static bool str_encodeURI(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "encodeURI"); + CallArgs args = CallArgsFromVp(argc, vp); + Rooted<JSLinearString*> str(cx, ArgToLinearString(cx, args, 0)); + if (!str) { + return false; + } + + return Encode(cx, str, js_isUriReservedPlusPound, args.rval()); +} + +static bool str_encodeURI_Component(JSContext* cx, unsigned argc, Value* vp) { + AutoJSMethodProfilerEntry pseudoFrame(cx, "encodeURIComponent"); + CallArgs args = CallArgsFromVp(argc, vp); + Rooted<JSLinearString*> str(cx, ArgToLinearString(cx, args, 0)); + if (!str) { + return false; + } + + return Encode(cx, str, nullptr, args.rval()); +} + +JSString* js::EncodeURI(JSContext* cx, const char* chars, size_t length) { + JSStringBuilder sb(cx); + EncodeResult result = Encode(sb, reinterpret_cast<const Latin1Char*>(chars), + length, js_isUriReservedPlusPound); + if (result == EncodeResult::Encode_Failure) { + return nullptr; + } + if (result == EncodeResult::Encode_BadUri) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BAD_URI); + return nullptr; + } + if (sb.empty()) { + return NewStringCopyN<CanGC>(cx, chars, length); + } + return sb.finishString(); +} + +static bool FlatStringMatchHelper(JSContext* cx, HandleString str, + HandleString pattern, bool* isFlat, + int32_t* match) { + Rooted<JSLinearString*> linearPattern(cx, pattern->ensureLinear(cx)); + if (!linearPattern) { + return false; + } + + static const size_t MAX_FLAT_PAT_LEN = 256; + if (linearPattern->length() > MAX_FLAT_PAT_LEN || + StringHasRegExpMetaChars(linearPattern)) { + *isFlat = false; + return true; + } + + *isFlat = true; + if (str->isRope()) { + if (!RopeMatch(cx, &str->asRope(), linearPattern, match)) { + return false; + } + } else { + *match = StringMatch(&str->asLinear(), linearPattern); + } + + return true; +} + +static bool BuildFlatMatchArray(JSContext* cx, HandleString str, + HandleString pattern, int32_t match, + MutableHandleValue rval) { + if (match < 0) { + rval.setNull(); + return true; + } + + // Get the templateObject that defines the shape and type of the output + // object. + ArrayObject* templateObject = + cx->realm()->regExps.getOrCreateMatchResultTemplateObject(cx); + if (!templateObject) { + return false; + } + + Rooted<ArrayObject*> arr( + cx, NewDenseFullyAllocatedArrayWithTemplate(cx, 1, templateObject)); + if (!arr) { + return false; + } + + // Store a Value for each pair. + arr->setDenseInitializedLength(1); + arr->initDenseElement(0, StringValue(pattern)); + + // Set the |index| property. (TemplateObject positions it in slot 0). + arr->setSlot(0, Int32Value(match)); + + // Set the |input| property. (TemplateObject positions it in slot 1). + arr->setSlot(1, StringValue(str)); + +#ifdef DEBUG + RootedValue test(cx); + RootedId id(cx, NameToId(cx->names().index)); + if (!NativeGetProperty(cx, arr, id, &test)) { + return false; + } + MOZ_ASSERT(test == arr->getSlot(0)); + id = NameToId(cx->names().input); + if (!NativeGetProperty(cx, arr, id, &test)) { + return false; + } + MOZ_ASSERT(test == arr->getSlot(1)); +#endif + + rval.setObject(*arr); + return true; +} + +#ifdef DEBUG +static bool CallIsStringOptimizable(JSContext* cx, const char* name, + bool* result) { + FixedInvokeArgs<0> args(cx); + + RootedValue rval(cx); + if (!CallSelfHostedFunction(cx, name, UndefinedHandleValue, args, &rval)) { + return false; + } + + *result = rval.toBoolean(); + return true; +} +#endif + +bool js::FlatStringMatch(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(args[0].isString()); + MOZ_ASSERT(args[1].isString()); +#ifdef DEBUG + bool isOptimizable = false; + if (!CallIsStringOptimizable(cx, "IsStringMatchOptimizable", + &isOptimizable)) { + return false; + } + MOZ_ASSERT(isOptimizable); +#endif + + RootedString str(cx, args[0].toString()); + RootedString pattern(cx, args[1].toString()); + + bool isFlat = false; + int32_t match = 0; + if (!FlatStringMatchHelper(cx, str, pattern, &isFlat, &match)) { + return false; + } + + if (!isFlat) { + args.rval().setUndefined(); + return true; + } + + return BuildFlatMatchArray(cx, str, pattern, match, args.rval()); +} + +bool js::FlatStringSearch(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(args[0].isString()); + MOZ_ASSERT(args[1].isString()); +#ifdef DEBUG + bool isOptimizable = false; + if (!CallIsStringOptimizable(cx, "IsStringSearchOptimizable", + &isOptimizable)) { + return false; + } + MOZ_ASSERT(isOptimizable); +#endif + + RootedString str(cx, args[0].toString()); + RootedString pattern(cx, args[1].toString()); + + bool isFlat = false; + int32_t match = 0; + if (!FlatStringMatchHelper(cx, str, pattern, &isFlat, &match)) { + return false; + } + + if (!isFlat) { + args.rval().setInt32(-2); + return true; + } + + args.rval().setInt32(match); + return true; +} diff --git a/js/src/builtin/String.h b/js/src/builtin/String.h new file mode 100644 index 0000000000..925439b873 --- /dev/null +++ b/js/src/builtin/String.h @@ -0,0 +1,106 @@ +/* -*- 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 builtin_String_h +#define builtin_String_h + +#include "NamespaceImports.h" + +#include "js/RootingAPI.h" +#include "js/Value.h" + +namespace js { + +class ArrayObject; +class GlobalObject; + +/* Initialize the String class, returning its prototype object. */ +extern JSObject* InitStringClass(JSContext* cx, Handle<GlobalObject*> global); + +extern bool str_fromCharCode(JSContext* cx, unsigned argc, Value* vp); + +extern bool str_fromCharCode_one_arg(JSContext* cx, HandleValue code, + MutableHandleValue rval); + +extern bool str_fromCodePoint(JSContext* cx, unsigned argc, Value* vp); + +extern bool str_fromCodePoint_one_arg(JSContext* cx, HandleValue code, + MutableHandleValue rval); + +// String methods exposed so they can be installed in the self-hosting global. + +extern bool str_includes(JSContext* cx, unsigned argc, Value* vp); + +extern bool str_indexOf(JSContext* cx, unsigned argc, Value* vp); + +extern bool str_startsWith(JSContext* cx, unsigned argc, Value* vp); + +extern bool str_toString(JSContext* cx, unsigned argc, Value* vp); + +extern bool str_charCodeAt_impl(JSContext* cx, HandleString string, + HandleValue index, MutableHandleValue res); + +extern bool str_charCodeAt(JSContext* cx, unsigned argc, Value* vp); + +extern bool str_endsWith(JSContext* cx, unsigned argc, Value* vp); + +#if JS_HAS_INTL_API +/** + * Returns the input string converted to lower case based on the language + * specific case mappings for the input locale. + * + * Usage: lowerCase = intl_toLocaleLowerCase(string, locale) + */ +[[nodiscard]] extern bool intl_toLocaleLowerCase(JSContext* cx, unsigned argc, + Value* vp); + +/** + * Returns the input string converted to upper case based on the language + * specific case mappings for the input locale. + * + * Usage: upperCase = intl_toLocaleUpperCase(string, locale) + */ +[[nodiscard]] extern bool intl_toLocaleUpperCase(JSContext* cx, unsigned argc, + Value* vp); +#endif + +ArrayObject* StringSplitString(JSContext* cx, HandleString str, + HandleString sep, uint32_t limit); + +JSString* StringFlatReplaceString(JSContext* cx, HandleString string, + HandleString pattern, + HandleString replacement); + +JSString* str_replace_string_raw(JSContext* cx, HandleString string, + HandleString pattern, + HandleString replacement); + +JSString* str_replaceAll_string_raw(JSContext* cx, HandleString string, + HandleString pattern, + HandleString replacement); + +extern bool StringIndexOf(JSContext* cx, HandleString string, + HandleString searchString, int32_t* result); + +extern bool StringStartsWith(JSContext* cx, HandleString string, + HandleString searchString, bool* result); + +extern bool StringEndsWith(JSContext* cx, HandleString string, + HandleString searchString, bool* result); + +extern JSString* StringToLowerCase(JSContext* cx, HandleString string); + +extern JSString* StringToUpperCase(JSContext* cx, HandleString string); + +extern bool StringConstructor(JSContext* cx, unsigned argc, Value* vp); + +extern bool FlatStringMatch(JSContext* cx, unsigned argc, Value* vp); + +extern bool FlatStringSearch(JSContext* cx, unsigned argc, Value* vp); + +} /* namespace js */ + +#endif /* builtin_String_h */ diff --git a/js/src/builtin/String.js b/js/src/builtin/String.js new file mode 100644 index 0000000000..386bd8d40a --- /dev/null +++ b/js/src/builtin/String.js @@ -0,0 +1,1203 @@ +/* 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/. */ + +function StringProtoHasNoMatch() { + var ObjectProto = GetBuiltinPrototype("Object"); + var StringProto = GetBuiltinPrototype("String"); + if (!ObjectHasPrototype(StringProto, ObjectProto)) { + return false; + } + return !(GetBuiltinSymbol("match") in StringProto); +} + +function IsStringMatchOptimizable() { + var RegExpProto = GetBuiltinPrototype("RegExp"); + // If RegExpPrototypeOptimizable succeeds, `exec` and `@@match` are + // guaranteed to be data properties. + return ( + RegExpPrototypeOptimizable(RegExpProto) && + RegExpProto.exec === RegExp_prototype_Exec && + RegExpProto[GetBuiltinSymbol("match")] === RegExpMatch + ); +} + +function ThrowIncompatibleMethod(name, thisv) { + ThrowTypeError(JSMSG_INCOMPATIBLE_PROTO, "String", name, ToString(thisv)); +} + +// ES 2016 draft Mar 25, 2016 21.1.3.11. +function String_match(regexp) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("match", this); + } + + // Step 2. + var isPatternString = typeof regexp === "string"; + if ( + !(isPatternString && StringProtoHasNoMatch()) && + !IsNullOrUndefined(regexp) + ) { + // Step 2.a. + var matcher = GetMethod(regexp, GetBuiltinSymbol("match")); + + // Step 2.b. + if (matcher !== undefined) { + return callContentFunction(matcher, regexp, this); + } + } + + // Step 3. + var S = ToString(this); + + if (isPatternString && IsStringMatchOptimizable()) { + var flatResult = FlatStringMatch(S, regexp); + if (flatResult !== undefined) { + return flatResult; + } + } + + // Step 4. + var rx = RegExpCreate(regexp); + + // Step 5 (optimized case). + if (IsStringMatchOptimizable()) { + return RegExpMatcher(rx, S, 0); + } + + // Step 5. + return callContentFunction(GetMethod(rx, GetBuiltinSymbol("match")), rx, S); +} + +// String.prototype.matchAll proposal. +// +// String.prototype.matchAll ( regexp ) +function String_matchAll(regexp) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("matchAll", this); + } + + // Step 2. + if (!IsNullOrUndefined(regexp)) { + // Steps 2.a-b. + if (IsRegExp(regexp)) { + // Step 2.b.i. + var flags = regexp.flags; + + // Step 2.b.ii. + if (IsNullOrUndefined(flags)) { + ThrowTypeError(JSMSG_FLAGS_UNDEFINED_OR_NULL); + } + + // Step 2.b.iii. + if (!callFunction(std_String_includes, ToString(flags), "g")) { + ThrowTypeError(JSMSG_REQUIRES_GLOBAL_REGEXP, "matchAll"); + } + } + + // Step 2.c. + var matcher = GetMethod(regexp, GetBuiltinSymbol("matchAll")); + + // Step 2.d. + if (matcher !== undefined) { + return callContentFunction(matcher, regexp, this); + } + } + + // Step 3. + var string = ToString(this); + + // Step 4. + var rx = RegExpCreate(regexp, "g"); + + // Step 5. + return callContentFunction( + GetMethod(rx, GetBuiltinSymbol("matchAll")), + rx, + string + ); +} + +/** + * A helper function implementing the logic for both String.prototype.padStart + * and String.prototype.padEnd as described in ES7 Draft March 29, 2016 + */ +function String_pad(maxLength, fillString, padEnd) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod(padEnd ? "padEnd" : "padStart", this); + } + + // Step 2. + let str = ToString(this); + + // Steps 3-4. + let intMaxLength = ToLength(maxLength); + let strLen = str.length; + + // Step 5. + if (intMaxLength <= strLen) { + return str; + } + + // Steps 6-7. + assert(fillString !== undefined, "never called when fillString is undefined"); + let filler = ToString(fillString); + + // Step 8. + if (filler === "") { + return str; + } + + // Throw an error if the final string length exceeds the maximum string + // length. Perform this check early so we can use int32 operations below. + if (intMaxLength > MAX_STRING_LENGTH) { + ThrowRangeError(JSMSG_RESULTING_STRING_TOO_LARGE); + } + + // Step 9. + let fillLen = intMaxLength - strLen; + + // Step 10. + // Perform an int32 division to ensure String_repeat is not called with a + // double to avoid repeated bailouts in ToInteger. + let truncatedStringFiller = callFunction( + String_repeat, + filler, + (fillLen / filler.length) | 0 + ); + + truncatedStringFiller += Substring(filler, 0, fillLen % filler.length); + + // Step 11. + if (padEnd === true) { + return str + truncatedStringFiller; + } + return truncatedStringFiller + str; +} + +function String_pad_start(maxLength, fillString = " ") { + return callFunction(String_pad, this, maxLength, fillString, false); +} + +function String_pad_end(maxLength, fillString = " ") { + return callFunction(String_pad, this, maxLength, fillString, true); +} + +function StringProtoHasNoReplace() { + var ObjectProto = GetBuiltinPrototype("Object"); + var StringProto = GetBuiltinPrototype("String"); + if (!ObjectHasPrototype(StringProto, ObjectProto)) { + return false; + } + return !(GetBuiltinSymbol("replace") in StringProto); +} + +// A thin wrapper to call SubstringKernel with int32-typed arguments. +// Caller should check the range of |from| and |length|. +function Substring(str, from, length) { + assert(typeof str === "string", "|str| should be a string"); + assert( + (from | 0) === from, + "coercing |from| into int32 should not change the value" + ); + assert( + (length | 0) === length, + "coercing |length| into int32 should not change the value" + ); + + return SubstringKernel(str, from | 0, length | 0); +} + +// ES 2016 draft Mar 25, 2016 21.1.3.14. +function String_replace(searchValue, replaceValue) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("replace", this); + } + + // Step 2. + if ( + !(typeof searchValue === "string" && StringProtoHasNoReplace()) && + !IsNullOrUndefined(searchValue) + ) { + // Step 2.a. + var replacer = GetMethod(searchValue, GetBuiltinSymbol("replace")); + + // Step 2.b. + if (replacer !== undefined) { + return callContentFunction(replacer, searchValue, this, replaceValue); + } + } + + // Step 3. + var string = ToString(this); + + // Step 4. + var searchString = ToString(searchValue); + + if (typeof replaceValue === "string") { + // Steps 6-12: Optimized for string case. + return StringReplaceString(string, searchString, replaceValue); + } + + // Step 5. + if (!IsCallable(replaceValue)) { + // Steps 6-12. + return StringReplaceString(string, searchString, ToString(replaceValue)); + } + + // Step 7. + var pos = callFunction(std_String_indexOf, string, searchString); + if (pos === -1) { + return string; + } + + // Step 8. + var replStr = ToString( + callContentFunction(replaceValue, undefined, searchString, pos, string) + ); + + // Step 10. + var tailPos = pos + searchString.length; + + // Step 11. + var newString; + if (pos === 0) { + newString = ""; + } else { + newString = Substring(string, 0, pos); + } + + newString += replStr; + var stringLength = string.length; + if (tailPos < stringLength) { + newString += Substring(string, tailPos, stringLength - tailPos); + } + + // Step 12. + return newString; +} + +// String.prototype.replaceAll (Stage 3 proposal) +// https://tc39.es/proposal-string-replaceall/ +// +// String.prototype.replaceAll ( searchValue, replaceValue ) +function String_replaceAll(searchValue, replaceValue) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("replaceAll", this); + } + + // Step 2. + if (!IsNullOrUndefined(searchValue)) { + // Steps 2.a-b. + if (IsRegExp(searchValue)) { + // Step 2.b.i. + var flags = searchValue.flags; + + // Step 2.b.ii. + if (IsNullOrUndefined(flags)) { + ThrowTypeError(JSMSG_FLAGS_UNDEFINED_OR_NULL); + } + + // Step 2.b.iii. + if (!callFunction(std_String_includes, ToString(flags), "g")) { + ThrowTypeError(JSMSG_REQUIRES_GLOBAL_REGEXP, "replaceAll"); + } + } + + // Step 2.c. + var replacer = GetMethod(searchValue, GetBuiltinSymbol("replace")); + + // Step 2.b. + if (replacer !== undefined) { + return callContentFunction(replacer, searchValue, this, replaceValue); + } + } + + // Step 3. + var string = ToString(this); + + // Step 4. + var searchString = ToString(searchValue); + + // Steps 5-6. + if (!IsCallable(replaceValue)) { + // Steps 7-16. + return StringReplaceAllString(string, searchString, ToString(replaceValue)); + } + + // Step 7. + var searchLength = searchString.length; + + // Step 8. + var advanceBy = std_Math_max(1, searchLength); + + // Step 9 (not needed in this implementation). + + // Step 12. + var endOfLastMatch = 0; + + // Step 13. + var result = ""; + + // Steps 10-11, 14. + var position = 0; + while (true) { + // Steps 10-11. + // + // StringIndexOf doesn't clamp the |position| argument to the input + // string length, i.e. |StringIndexOf("abc", "", 4)| returns -1, + // whereas |"abc".indexOf("", 4)| returns 3. That means we need to + // exit the loop when |nextPosition| is smaller than |position| and + // not just when |nextPosition| is -1. + var nextPosition = callFunction( + std_String_indexOf, + string, + searchString, + position + ); + if (nextPosition < position) { + break; + } + position = nextPosition; + + // Step 14.a. + var replacement = ToString( + callContentFunction( + replaceValue, + undefined, + searchString, + position, + string + ) + ); + + // Step 14.b (not applicable). + + // Step 14.c. + var stringSlice = Substring( + string, + endOfLastMatch, + position - endOfLastMatch + ); + + // Step 14.d. + result += stringSlice + replacement; + + // Step 14.e. + endOfLastMatch = position + searchLength; + + // Step 11.b. + position += advanceBy; + } + + // Step 15. + if (endOfLastMatch < string.length) { + // Step 15.a. + result += Substring(string, endOfLastMatch, string.length - endOfLastMatch); + } + + // Step 16. + return result; +} + +function StringProtoHasNoSearch() { + var ObjectProto = GetBuiltinPrototype("Object"); + var StringProto = GetBuiltinPrototype("String"); + if (!ObjectHasPrototype(StringProto, ObjectProto)) { + return false; + } + return !(GetBuiltinSymbol("search") in StringProto); +} + +function IsStringSearchOptimizable() { + var RegExpProto = GetBuiltinPrototype("RegExp"); + // If RegExpPrototypeOptimizable succeeds, `exec` and `@@search` are + // guaranteed to be data properties. + return ( + RegExpPrototypeOptimizable(RegExpProto) && + RegExpProto.exec === RegExp_prototype_Exec && + RegExpProto[GetBuiltinSymbol("search")] === RegExpSearch + ); +} + +// ES 2016 draft Mar 25, 2016 21.1.3.15. +function String_search(regexp) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("search", this); + } + + // Step 2. + var isPatternString = typeof regexp === "string"; + if ( + !(isPatternString && StringProtoHasNoSearch()) && + !IsNullOrUndefined(regexp) + ) { + // Step 2.a. + var searcher = GetMethod(regexp, GetBuiltinSymbol("search")); + + // Step 2.b. + if (searcher !== undefined) { + return callContentFunction(searcher, regexp, this); + } + } + + // Step 3. + var string = ToString(this); + + if (isPatternString && IsStringSearchOptimizable()) { + var flatResult = FlatStringSearch(string, regexp); + if (flatResult !== -2) { + return flatResult; + } + } + + // Step 4. + var rx = RegExpCreate(regexp); + + // Step 5. + return callContentFunction( + GetMethod(rx, GetBuiltinSymbol("search")), + rx, + string + ); +} + +function StringProtoHasNoSplit() { + var ObjectProto = GetBuiltinPrototype("Object"); + var StringProto = GetBuiltinPrototype("String"); + if (!ObjectHasPrototype(StringProto, ObjectProto)) { + return false; + } + return !(GetBuiltinSymbol("split") in StringProto); +} + +// ES 2016 draft Mar 25, 2016 21.1.3.17. +function String_split(separator, limit) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("split", this); + } + + // Optimized path for string.split(string), especially when both strings + // are constants. Following sequence of if's cannot be put together in + // order that IonMonkey sees the constant if present (bug 1246141). + if (typeof this === "string") { + if (StringProtoHasNoSplit()) { + if (typeof separator === "string") { + if (limit === undefined) { + // inlineConstantStringSplitString needs both arguments to + // be MConstant, so pass them directly. + return StringSplitString(this, separator); + } + } + } + } + + // Step 2. + if ( + !(typeof separator === "string" && StringProtoHasNoSplit()) && + !IsNullOrUndefined(separator) + ) { + // Step 2.a. + var splitter = GetMethod(separator, GetBuiltinSymbol("split")); + + // Step 2.b. + if (splitter !== undefined) { + return callContentFunction(splitter, separator, this, limit); + } + } + + // Step 3. + var S = ToString(this); + + // Step 6. + var R; + if (limit !== undefined) { + var lim = limit >>> 0; + + // Step 9. + R = ToString(separator); + + // Step 10. + if (lim === 0) { + return []; + } + + // Step 11. + if (separator === undefined) { + return [S]; + } + + // Steps 4, 8, 12-18. + return StringSplitStringLimit(S, R, lim); + } + + // Step 9. + R = ToString(separator); + + // Step 11. + if (separator === undefined) { + return [S]; + } + + // Optimized path. + // Steps 4, 8, 12-18. + return StringSplitString(S, R); +} + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 21.1.3.22 String.prototype.substring ( start, end ) +function String_substring(start, end) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("substring", this); + } + + // Step 2. + var str = ToString(this); + + // Step 3. + var len = str.length; + + // Step 4. + var intStart = ToInteger(start); + + // Step 5. + var intEnd = end === undefined ? len : ToInteger(end); + + // Step 6. + var finalStart = std_Math_min(std_Math_max(intStart, 0), len); + + // Step 7. + var finalEnd = std_Math_min(std_Math_max(intEnd, 0), len); + + // Step 8. + var from = std_Math_min(finalStart, finalEnd); + + // Step 9. + var to = std_Math_max(finalStart, finalEnd); + + // Step 10. + // While |from| and |to - from| are bounded to the length of |str| and this + // and thus definitely in the int32 range, they can still be typed as + // double. Eagerly truncate since SubstringKernel only accepts int32. + return SubstringKernel(str, from | 0, (to - from) | 0); +} +SetIsInlinableLargeFunction(String_substring); + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// B.2.3.1 String.prototype.substr ( start, length ) +function String_substr(start, length) { + // Steps 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("substr", this); + } + + // Step 2. + var str = ToString(this); + + // Step 3. + var intStart = ToInteger(start); + + // Steps 4-5. + var size = str.length; + // Use |size| instead of +Infinity to avoid performing calculations with + // doubles. (The result is the same either way.) + var end = length === undefined ? size : ToInteger(length); + + // Step 6. + if (intStart < 0) { + intStart = std_Math_max(intStart + size, 0); + } else { + // Restrict the input range to allow better Ion optimizations. + intStart = std_Math_min(intStart, size); + } + + // Step 7. + var resultLength = std_Math_min(std_Math_max(end, 0), size - intStart); + + // Step 8. + assert( + 0 <= resultLength && resultLength <= size - intStart, + "resultLength is a valid substring length value" + ); + + // Step 9. + // While |intStart| and |resultLength| are bounded to the length of |str| + // and thus definitely in the int32 range, they can still be typed as + // double. Eagerly truncate since SubstringKernel only accepts int32. + return SubstringKernel(str, intStart | 0, resultLength | 0); +} +SetIsInlinableLargeFunction(String_substr); + +// ES2021 draft rev 12a546b92275a0e2f834017db2727bb9c6f6c8fd +// 21.1.3.4 String.prototype.concat ( ...args ) +// Note: String.prototype.concat.length is 1. +function String_concat(arg1) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("concat", this); + } + + // Step 2. + var str = ToString(this); + + // Specialize for the most common number of arguments for better inlining. + if (ArgumentsLength() === 0) { + return str; + } + if (ArgumentsLength() === 1) { + return str + ToString(GetArgument(0)); + } + if (ArgumentsLength() === 2) { + return str + ToString(GetArgument(0)) + ToString(GetArgument(1)); + } + + // Step 3. (implicit) + // Step 4. + var result = str; + + // Step 5. + for (var i = 0; i < ArgumentsLength(); i++) { + // Steps 5.a-b. + var nextString = ToString(GetArgument(i)); + // Step 5.c. + result += nextString; + } + + // Step 6. + return result; +} + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 21.1.3.19 String.prototype.slice ( start, end ) +function String_slice(start, end) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("slice", this); + } + + // Step 2. + var str = ToString(this); + + // Step 3. + var len = str.length; + + // Step 4. + var intStart = ToInteger(start); + + // Step 5. + var intEnd = end === undefined ? len : ToInteger(end); + + // Step 6. + var from = + intStart < 0 + ? std_Math_max(len + intStart, 0) + : std_Math_min(intStart, len); + + // Step 7. + var to = + intEnd < 0 ? std_Math_max(len + intEnd, 0) : std_Math_min(intEnd, len); + + // Step 8. + var span = std_Math_max(to - from, 0); + + // Step 9. + // While |from| and |span| are bounded to the length of |str| + // and thus definitely in the int32 range, they can still be typed as + // double. Eagerly truncate since SubstringKernel only accepts int32. + return SubstringKernel(str, from | 0, span | 0); +} +SetIsInlinableLargeFunction(String_slice); + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 21.1.3.3 String.prototype.codePointAt ( pos ) +function String_codePointAt(pos) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("codePointAt", this); + } + + // Step 2. + var S = ToString(this); + + // Step 3. + var position = ToInteger(pos); + + // Step 4. + var size = S.length; + + // Step 5. + if (position < 0 || position >= size) { + return undefined; + } + + // Steps 6-7. + var first = callFunction(std_String_charCodeAt, S, position); + if (first < 0xd800 || first > 0xdbff || position + 1 === size) { + return first; + } + + // Steps 8-9. + var second = callFunction(std_String_charCodeAt, S, position + 1); + if (second < 0xdc00 || second > 0xdfff) { + return first; + } + + // Step 10. + return (first - 0xd800) * 0x400 + (second - 0xdc00) + 0x10000; +} + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 21.1.3.16 String.prototype.repeat ( count ) +function String_repeat(count) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("repeat", this); + } + + // Step 2. + var S = ToString(this); + + // Step 3. + var n = ToInteger(count); + + // Step 4. + if (n < 0) { + ThrowRangeError(JSMSG_NEGATIVE_REPETITION_COUNT); + } + + // Step 5. + // Inverted condition to handle |Infinity * 0 = NaN| correctly. + if (!(n * S.length <= MAX_STRING_LENGTH)) { + ThrowRangeError(JSMSG_RESULTING_STRING_TOO_LARGE); + } + + // Communicate |n|'s possible range to the compiler. We actually use + // MAX_STRING_LENGTH + 1 as range because that's a valid bit mask. That's + // fine because it's only used as optimization hint. + assert( + TO_INT32(MAX_STRING_LENGTH + 1) === MAX_STRING_LENGTH + 1, + "MAX_STRING_LENGTH + 1 must fit in int32" + ); + assert( + ((MAX_STRING_LENGTH + 1) & (MAX_STRING_LENGTH + 2)) === 0, + "MAX_STRING_LENGTH + 1 can be used as a bitmask" + ); + n = n & (MAX_STRING_LENGTH + 1); + + // Steps 6-7. + var T = ""; + for (;;) { + if (n & 1) { + T += S; + } + n >>= 1; + if (n) { + S += S; + } else { + break; + } + } + return T; +} + +// ES6 draft specification, section 21.1.3.27, version 2013-09-27. +function String_iterator() { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowTypeError( + JSMSG_INCOMPATIBLE_PROTO2, + "String", + "Symbol.iterator", + ToString(this) + ); + } + + // Step 2. + var S = ToString(this); + + // Step 3. + var iterator = NewStringIterator(); + UnsafeSetReservedSlot(iterator, ITERATOR_SLOT_TARGET, S); + UnsafeSetReservedSlot(iterator, ITERATOR_SLOT_NEXT_INDEX, 0); + return iterator; +} + +function StringIteratorNext() { + var obj = this; + if (!IsObject(obj) || (obj = GuardToStringIterator(obj)) === null) { + return callFunction( + CallStringIteratorMethodIfWrapped, + this, + "StringIteratorNext" + ); + } + + var S = UnsafeGetStringFromReservedSlot(obj, ITERATOR_SLOT_TARGET); + // We know that JSString::MAX_LENGTH <= INT32_MAX (and assert this in + // SelfHostring.cpp) so our current index can never be anything other than + // an Int32Value. + var index = UnsafeGetInt32FromReservedSlot(obj, ITERATOR_SLOT_NEXT_INDEX); + var size = S.length; + var result = { value: undefined, done: false }; + + if (index >= size) { + result.done = true; + return result; + } + + var charCount = 1; + var first = callFunction(std_String_charCodeAt, S, index); + if (first >= 0xd800 && first <= 0xdbff && index + 1 < size) { + var second = callFunction(std_String_charCodeAt, S, index + 1); + if (second >= 0xdc00 && second <= 0xdfff) { + first = (first - 0xd800) * 0x400 + (second - 0xdc00) + 0x10000; + charCount = 2; + } + } + + UnsafeSetReservedSlot(obj, ITERATOR_SLOT_NEXT_INDEX, index + charCount); + + // Communicate |first|'s possible range to the compiler. + result.value = callFunction(std_String_fromCodePoint, null, first & 0x1fffff); + + return result; +} + +#if JS_HAS_INTL_API +var collatorCache = new_Record(); + +/** + * Compare this String against that String, using the locale and collation + * options provided. + * + * Spec: ECMAScript Internationalization API Specification, 13.1.1. + */ +function String_localeCompare(that) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("localeCompare", this); + } + + // Steps 2-3. + var S = ToString(this); + var That = ToString(that); + + // Steps 4-5. + var locales = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + var options = ArgumentsLength() > 2 ? GetArgument(2) : undefined; + + // Step 6. + var collator; + if (locales === undefined && options === undefined) { + // This cache only optimizes for the old ES5 localeCompare without + // locales and options. + if (!intl_IsRuntimeDefaultLocale(collatorCache.runtimeDefaultLocale)) { + collatorCache.collator = intl_Collator(locales, options); + collatorCache.runtimeDefaultLocale = intl_RuntimeDefaultLocale(); + } + collator = collatorCache.collator; + } else { + collator = intl_Collator(locales, options); + } + + // Step 7. + return intl_CompareStrings(collator, S, That); +} + +/** + * 13.1.2 String.prototype.toLocaleLowerCase ( [ locales ] ) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +function String_toLocaleLowerCase() { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("toLocaleLowerCase", this); + } + + // Step 2. + var string = ToString(this); + + // Handle the common cases (no locales argument or a single string + // argument) first. + var locales = ArgumentsLength() ? GetArgument(0) : undefined; + var requestedLocale; + if (locales === undefined) { + // Steps 3, 6. + requestedLocale = undefined; + } else if (typeof locales === "string") { + // Steps 3, 5. + requestedLocale = intl_ValidateAndCanonicalizeLanguageTag(locales, false); + } else { + // Step 3. + var requestedLocales = CanonicalizeLocaleList(locales); + + // Steps 4-6. + requestedLocale = requestedLocales.length ? requestedLocales[0] : undefined; + } + + // Trivial case: When the input is empty, directly return the empty string. + if (string.length === 0) { + return ""; + } + + if (requestedLocale === undefined) { + requestedLocale = DefaultLocale(); + } + + // Steps 7-16. + return intl_toLocaleLowerCase(string, requestedLocale); +} + +/** + * 13.1.3 String.prototype.toLocaleUpperCase ( [ locales ] ) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +function String_toLocaleUpperCase() { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("toLocaleUpperCase", this); + } + + // Step 2. + var string = ToString(this); + + // Handle the common cases (no locales argument or a single string + // argument) first. + var locales = ArgumentsLength() ? GetArgument(0) : undefined; + var requestedLocale; + if (locales === undefined) { + // Steps 3, 6. + requestedLocale = undefined; + } else if (typeof locales === "string") { + // Steps 3, 5. + requestedLocale = intl_ValidateAndCanonicalizeLanguageTag(locales, false); + } else { + // Step 3. + var requestedLocales = CanonicalizeLocaleList(locales); + + // Steps 4-6. + requestedLocale = requestedLocales.length ? requestedLocales[0] : undefined; + } + + // Trivial case: When the input is empty, directly return the empty string. + if (string.length === 0) { + return ""; + } + + if (requestedLocale === undefined) { + requestedLocale = DefaultLocale(); + } + + // Steps 7-16. + return intl_toLocaleUpperCase(string, requestedLocale); +} +#endif // JS_HAS_INTL_API + +// ES2018 draft rev 8fadde42cf6a9879b4ab0cb6142b31c4ee501667 +// 21.1.2.4 String.raw ( template, ...substitutions ) +function String_static_raw(callSite /*, ...substitutions*/) { + // Steps 1-2 (not applicable). + + // Step 3. + var cooked = ToObject(callSite); + + // Step 4. + var raw = ToObject(cooked.raw); + + // Step 5. + var literalSegments = ToLength(raw.length); + + // Step 6. + if (literalSegments === 0) { + return ""; + } + + // Special case for |String.raw `<literal>`| callers to avoid falling into + // the loop code below. + if (literalSegments === 1) { + return ToString(raw[0]); + } + + // Steps 7-9 were reordered to use ArgumentsLength/GetArgument instead of a + // rest parameter, because the former is currently more optimized. + // + // String.raw intersperses the substitution elements between the literal + // segments, i.e. a substitution is added iff there are still pending + // literal segments. Furthermore by moving the access to |raw[0]| outside + // of the loop, we can use |nextIndex| to index into both, the |raw| array + // and the arguments. + + // Steps 7 (implicit) and 9.a-c. + var resultString = ToString(raw[0]); + + // Steps 8-9, 9.d, and 9.i. + for (var nextIndex = 1; nextIndex < literalSegments; nextIndex++) { + // Steps 9.e-h. + if (nextIndex < ArgumentsLength()) { + resultString += ToString(GetArgument(nextIndex)); + } + + // Steps 9.a-c. + resultString += ToString(raw[nextIndex]); + } + + // Step 9.d.i. + return resultString; +} + +// https://github.com/tc39/proposal-relative-indexing-method +// String.prototype.at ( index ) +function String_at(index) { + // Step 1. + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("at", this); + } + + // Step 2. + var string = ToString(this); + + // Step 3. + var len = string.length; + + // Step 4. + var relativeIndex = ToInteger(index); + + // Steps 5-6. + var k; + if (relativeIndex >= 0) { + k = relativeIndex; + } else { + k = len + relativeIndex; + } + + // Step 7. + if (k < 0 || k >= len) { + return undefined; + } + + // Step 8. + return string[k]; +} + +// ES6 draft 2014-04-27 B.2.3.3 +function String_big() { + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("big", this); + } + return "<big>" + ToString(this) + "</big>"; +} + +// ES6 draft 2014-04-27 B.2.3.4 +function String_blink() { + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("blink", this); + } + return "<blink>" + ToString(this) + "</blink>"; +} + +// ES6 draft 2014-04-27 B.2.3.5 +function String_bold() { + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("bold", this); + } + return "<b>" + ToString(this) + "</b>"; +} + +// ES6 draft 2014-04-27 B.2.3.6 +function String_fixed() { + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("fixed", this); + } + return "<tt>" + ToString(this) + "</tt>"; +} + +// ES6 draft 2014-04-27 B.2.3.9 +function String_italics() { + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("italics", this); + } + return "<i>" + ToString(this) + "</i>"; +} + +// ES6 draft 2014-04-27 B.2.3.11 +function String_small() { + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("small", this); + } + return "<small>" + ToString(this) + "</small>"; +} + +// ES6 draft 2014-04-27 B.2.3.12 +function String_strike() { + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("strike", this); + } + return "<strike>" + ToString(this) + "</strike>"; +} + +// ES6 draft 2014-04-27 B.2.3.13 +function String_sub() { + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("sub", this); + } + return "<sub>" + ToString(this) + "</sub>"; +} + +// ES6 draft 2014-04-27 B.2.3.14 +function String_sup() { + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("sup", this); + } + return "<sup>" + ToString(this) + "</sup>"; +} + +function EscapeAttributeValue(v) { + var inputStr = ToString(v); + return StringReplaceAllString(inputStr, '"', """); +} + +// ES6 draft 2014-04-27 B.2.3.2 +function String_anchor(name) { + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("anchor", this); + } + var S = ToString(this); + return '<a name="' + EscapeAttributeValue(name) + '">' + S + "</a>"; +} + +// ES6 draft 2014-04-27 B.2.3.7 +function String_fontcolor(color) { + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("fontcolor", this); + } + var S = ToString(this); + return '<font color="' + EscapeAttributeValue(color) + '">' + S + "</font>"; +} + +// ES6 draft 2014-04-27 B.2.3.8 +function String_fontsize(size) { + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("fontsize", this); + } + var S = ToString(this); + return '<font size="' + EscapeAttributeValue(size) + '">' + S + "</font>"; +} + +// ES6 draft 2014-04-27 B.2.3.10 +function String_link(url) { + if (IsNullOrUndefined(this)) { + ThrowIncompatibleMethod("link", this); + } + var S = ToString(this); + return '<a href="' + EscapeAttributeValue(url) + '">' + S + "</a>"; +} diff --git a/js/src/builtin/Symbol.cpp b/js/src/builtin/Symbol.cpp new file mode 100644 index 0000000000..b48d6b052d --- /dev/null +++ b/js/src/builtin/Symbol.cpp @@ -0,0 +1,235 @@ +/* -*- 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 "builtin/Symbol.h" +#include "js/Symbol.h" + +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/PropertySpec.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/SymbolType.h" + +#include "vm/JSObject-inl.h" + +using namespace js; + +const JSClass SymbolObject::class_ = { + "Symbol", + JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS) | + JSCLASS_HAS_CACHED_PROTO(JSProto_Symbol), + JS_NULL_CLASS_OPS, &SymbolObject::classSpec_}; + +// This uses PlainObject::class_ because: "The Symbol prototype object is an +// ordinary object. It is not a Symbol instance and does not have a +// [[SymbolData]] internal slot." (ES6 rev 24, 19.4.3) +const JSClass& SymbolObject::protoClass_ = PlainObject::class_; + +SymbolObject* SymbolObject::create(JSContext* cx, JS::HandleSymbol symbol) { + SymbolObject* obj = NewBuiltinClassInstance<SymbolObject>(cx); + if (!obj) { + return nullptr; + } + obj->setPrimitiveValue(symbol); + return obj; +} + +const JSPropertySpec SymbolObject::properties[] = { + JS_PSG("description", descriptionGetter, 0), + JS_STRING_SYM_PS(toStringTag, "Symbol", JSPROP_READONLY), JS_PS_END}; + +const JSFunctionSpec SymbolObject::methods[] = { + JS_FN(js_toString_str, toString, 0, 0), + JS_FN(js_valueOf_str, valueOf, 0, 0), + JS_SYM_FN(toPrimitive, toPrimitive, 1, JSPROP_READONLY), JS_FS_END}; + +const JSFunctionSpec SymbolObject::staticMethods[] = { + JS_FN("for", for_, 1, 0), JS_FN("keyFor", keyFor, 1, 0), JS_FS_END}; + +static bool SymbolClassFinish(JSContext* cx, HandleObject ctor, + HandleObject proto) { + Handle<NativeObject*> nativeCtor = ctor.as<NativeObject>(); + + // Define the well-known symbol properties, such as Symbol.iterator. + ImmutableTenuredPtr<PropertyName*>* names = + cx->names().wellKnownSymbolNames(); + RootedValue value(cx); + unsigned attrs = JSPROP_READONLY | JSPROP_PERMANENT; + WellKnownSymbols* wks = cx->runtime()->wellKnownSymbols; + for (size_t i = 0; i < JS::WellKnownSymbolLimit; i++) { + value.setSymbol(wks->get(i)); + if (!NativeDefineDataProperty(cx, nativeCtor, names[i], value, attrs)) { + return false; + } + } + return true; +} + +const ClassSpec SymbolObject::classSpec_ = { + GenericCreateConstructor<SymbolObject::construct, 0, + gc::AllocKind::FUNCTION>, + GenericCreatePrototype<SymbolObject>, + staticMethods, + nullptr, + methods, + properties, + SymbolClassFinish}; + +// ES2020 draft rev ecb4178012d6b4d9abc13fcbd45f5c6394b832ce +// 19.4.1.1 Symbol ( [ description ] ) +bool SymbolObject::construct(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (args.isConstructing()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_CONSTRUCTOR, "Symbol"); + return false; + } + + // Steps 2-3. + RootedString desc(cx); + if (!args.get(0).isUndefined()) { + desc = ToString(cx, args.get(0)); + if (!desc) { + return false; + } + } + + // Step 4. + JS::Symbol* symbol = JS::Symbol::new_(cx, JS::SymbolCode::UniqueSymbol, desc); + if (!symbol) { + return false; + } + args.rval().setSymbol(symbol); + return true; +} + +// ES2020 draft rev ecb4178012d6b4d9abc13fcbd45f5c6394b832ce +// 19.4.2.2 Symbol.for ( key ) +bool SymbolObject::for_(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + RootedString stringKey(cx, ToString(cx, args.get(0))); + if (!stringKey) { + return false; + } + + // Steps 2-6. + JS::Symbol* symbol = JS::Symbol::for_(cx, stringKey); + if (!symbol) { + return false; + } + args.rval().setSymbol(symbol); + return true; +} + +// ES2020 draft rev ecb4178012d6b4d9abc13fcbd45f5c6394b832ce +// 19.4.2.6 Symbol.keyFor ( sym ) +bool SymbolObject::keyFor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + HandleValue arg = args.get(0); + if (!arg.isSymbol()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, arg, + nullptr, "not a symbol"); + return false; + } + + // Step 2. + if (arg.toSymbol()->code() == JS::SymbolCode::InSymbolRegistry) { +#ifdef DEBUG + RootedString desc(cx, arg.toSymbol()->description()); + MOZ_ASSERT(JS::Symbol::for_(cx, desc) == arg.toSymbol()); +#endif + args.rval().setString(arg.toSymbol()->description()); + return true; + } + + // Step 3: omitted. + // Step 4. + args.rval().setUndefined(); + return true; +} + +static MOZ_ALWAYS_INLINE bool IsSymbol(HandleValue v) { + return v.isSymbol() || (v.isObject() && v.toObject().is<SymbolObject>()); +} + +// ES2020 draft rev ecb4178012d6b4d9abc13fcbd45f5c6394b832ce +// 19.4.3 Properties of the Symbol Prototype Object, thisSymbolValue. +static MOZ_ALWAYS_INLINE JS::Symbol* ThisSymbolValue(HandleValue val) { + // Step 3, the error case, is handled by CallNonGenericMethod. + MOZ_ASSERT(IsSymbol(val)); + + // Step 1. + if (val.isSymbol()) { + return val.toSymbol(); + } + + // Step 2. + return val.toObject().as<SymbolObject>().unbox(); +} + +// ES2020 draft rev ecb4178012d6b4d9abc13fcbd45f5c6394b832ce +// 19.4.3.3 Symbol.prototype.toString ( ) +bool SymbolObject::toString_impl(JSContext* cx, const CallArgs& args) { + // Step 1. + JS::Symbol* sym = ThisSymbolValue(args.thisv()); + + // Step 2. + return SymbolDescriptiveString(cx, sym, args.rval()); +} + +bool SymbolObject::toString(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsSymbol, toString_impl>(cx, args); +} + +// ES2020 draft rev ecb4178012d6b4d9abc13fcbd45f5c6394b832ce +// 19.4.3.4 Symbol.prototype.valueOf ( ) +bool SymbolObject::valueOf_impl(JSContext* cx, const CallArgs& args) { + // Step 1. + args.rval().setSymbol(ThisSymbolValue(args.thisv())); + return true; +} + +bool SymbolObject::valueOf(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsSymbol, valueOf_impl>(cx, args); +} + +// ES2020 draft rev ecb4178012d6b4d9abc13fcbd45f5c6394b832ce +// 19.4.3.5 Symbol.prototype [ @@toPrimitive ] ( hint ) +bool SymbolObject::toPrimitive(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // The specification gives exactly the same algorithm for @@toPrimitive as + // for valueOf, so reuse the valueOf implementation. + return CallNonGenericMethod<IsSymbol, valueOf_impl>(cx, args); +} + +// ES2020 draft rev ecb4178012d6b4d9abc13fcbd45f5c6394b832ce +// 19.4.3.2 get Symbol.prototype.description +bool SymbolObject::descriptionGetter_impl(JSContext* cx, const CallArgs& args) { + // Steps 1-2. + JS::Symbol* sym = ThisSymbolValue(args.thisv()); + + // Step 3. + // Return the symbol's description if present, otherwise return undefined. + if (JSString* str = sym->description()) { + args.rval().setString(str); + } else { + args.rval().setUndefined(); + } + return true; +} + +bool SymbolObject::descriptionGetter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsSymbol, descriptionGetter_impl>(cx, args); +} diff --git a/js/src/builtin/Symbol.h b/js/src/builtin/Symbol.h new file mode 100644 index 0000000000..7da3a561b0 --- /dev/null +++ b/js/src/builtin/Symbol.h @@ -0,0 +1,71 @@ +/* -*- 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 builtin_Symbol_h +#define builtin_Symbol_h + +#include "vm/NativeObject.h" + +namespace JS { +class Symbol; +} + +namespace js { + +class SymbolObject : public NativeObject { + /* Stores this Symbol object's [[PrimitiveValue]]. */ + static const unsigned PRIMITIVE_VALUE_SLOT = 0; + + public: + static const unsigned RESERVED_SLOTS = 1; + + static const JSClass class_; + static const JSClass& protoClass_; + + /* + * Creates a new Symbol object boxing the given primitive Symbol. The + * object's [[Prototype]] is determined from context. + */ + static SymbolObject* create(JSContext* cx, JS::HandleSymbol symbol); + + JS::Symbol* unbox() const { + return getFixedSlot(PRIMITIVE_VALUE_SLOT).toSymbol(); + } + + private: + inline void setPrimitiveValue(JS::Symbol* symbol) { + setFixedSlot(PRIMITIVE_VALUE_SLOT, SymbolValue(symbol)); + } + + [[nodiscard]] static bool construct(JSContext* cx, unsigned argc, Value* vp); + + // Static methods. + [[nodiscard]] static bool for_(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool keyFor(JSContext* cx, unsigned argc, Value* vp); + + // Methods defined on Symbol.prototype. + [[nodiscard]] static bool toString_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool toString(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool valueOf_impl(JSContext* cx, const CallArgs& args); + [[nodiscard]] static bool valueOf(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static bool toPrimitive(JSContext* cx, unsigned argc, + Value* vp); + + // Properties defined on Symbol.prototype. + [[nodiscard]] static bool descriptionGetter_impl(JSContext* cx, + const CallArgs& args); + [[nodiscard]] static bool descriptionGetter(JSContext* cx, unsigned argc, + Value* vp); + + static const JSPropertySpec properties[]; + static const JSFunctionSpec methods[]; + static const JSFunctionSpec staticMethods[]; + static const ClassSpec classSpec_; +}; + +} /* namespace js */ + +#endif /* builtin_Symbol_h */ diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp new file mode 100644 index 0000000000..d9034c886d --- /dev/null +++ b/js/src/builtin/TestingFunctions.cpp @@ -0,0 +1,9817 @@ +/* -*- 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 "builtin/TestingFunctions.h" + +#include "mozilla/Atomics.h" +#include "mozilla/Casting.h" +#include "mozilla/FloatingPoint.h" +#ifdef JS_HAS_INTL_API +# include "mozilla/intl/ICU4CLibrary.h" +# include "mozilla/intl/Locale.h" +# include "mozilla/intl/String.h" +# include "mozilla/intl/TimeZone.h" +#endif +#include "mozilla/Maybe.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Span.h" +#include "mozilla/Sprintf.h" +#include "mozilla/TextUtils.h" +#include "mozilla/ThreadLocal.h" + +#include <algorithm> +#include <cfloat> +#include <cinttypes> +#include <cmath> +#include <cstdlib> +#include <ctime> +#include <functional> +#include <initializer_list> +#include <iterator> +#include <utility> + +#if defined(XP_UNIX) && !defined(XP_DARWIN) +# include <time.h> +#else +# include <chrono> +#endif + +#include "fdlibm.h" +#include "jsapi.h" +#include "jsfriendapi.h" + +#ifdef JS_HAS_INTL_API +# include "builtin/intl/CommonFunctions.h" +# include "builtin/intl/FormatBuffer.h" +# include "builtin/intl/SharedIntlData.h" +#endif +#include "builtin/BigInt.h" +#include "builtin/MapObject.h" +#include "builtin/Promise.h" +#include "builtin/TestingUtility.h" // js::ParseCompileOptions, js::ParseDebugMetadata +#include "frontend/BytecodeCompilation.h" // frontend::CompileGlobalScriptToExtensibleStencil, frontend::DelazifyCanonicalScriptedFunction +#include "frontend/BytecodeCompiler.h" // frontend::ParseModuleToExtensibleStencil +#include "frontend/CompilationStencil.h" // frontend::CompilationStencil +#include "frontend/FrontendContext.h" // AutoReportFrontendContext +#include "gc/Allocator.h" +#include "gc/GC.h" +#include "gc/GCLock.h" +#include "gc/Zone.h" +#include "jit/BaselineJIT.h" +#include "jit/Disassemble.h" +#include "jit/InlinableNatives.h" +#include "jit/Invalidation.h" +#include "jit/Ion.h" +#include "jit/JitOptions.h" +#include "jit/JitRuntime.h" +#include "jit/TrialInlining.h" +#include "js/Array.h" // JS::NewArrayObject +#include "js/ArrayBuffer.h" // JS::{DetachArrayBuffer,GetArrayBufferLengthAndData,NewArrayBufferWithContents} +#include "js/CallAndConstruct.h" // JS::Call, JS::IsCallable, JS::IsConstructor, JS_CallFunction +#include "js/CharacterEncoding.h" +#include "js/CompilationAndEvaluation.h" +#include "js/CompileOptions.h" +#include "js/Date.h" +#include "js/experimental/CodeCoverage.h" // js::GetCodeCoverageSummary +#include "js/experimental/CompileScript.h" // JS::ParseGlobalScript, JS::PrepareForInstantiate +#include "js/experimental/JSStencil.h" // JS::Stencil +#include "js/experimental/PCCountProfiling.h" // JS::{Start,Stop}PCCountProfiling, JS::PurgePCCounts, JS::GetPCCountScript{Count,Summary,Contents} +#include "js/experimental/TypedData.h" // JS_GetObjectAsUint8Array +#include "js/friend/DumpFunctions.h" // js::Dump{Backtrace,Heap,Object}, JS::FormatStackDump, js::IgnoreNurseryObjects +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/friend/WindowProxy.h" // js::ToWindowProxyIfWindow +#include "js/GlobalObject.h" +#include "js/HashTable.h" +#include "js/Interrupt.h" +#include "js/LocaleSensitive.h" +#include "js/Printf.h" +#include "js/PropertyAndElement.h" // JS_DefineProperties, JS_DefineProperty, JS_DefinePropertyById, JS_Enumerate, JS_GetProperty, JS_GetPropertyById, JS_HasProperty, JS_SetElement, JS_SetProperty +#include "js/PropertySpec.h" +#include "js/SourceText.h" +#include "js/StableStringChars.h" +#include "js/Stack.h" +#include "js/String.h" // JS::GetLinearStringLength, JS::StringToLinearString +#include "js/StructuredClone.h" +#include "js/UbiNode.h" +#include "js/UbiNodeBreadthFirst.h" +#include "js/UbiNodeShortestPaths.h" +#include "js/UniquePtr.h" +#include "js/Vector.h" +#include "js/Wrapper.h" +#include "threading/CpuCount.h" +#include "util/DifferentialTesting.h" +#include "util/StringBuffer.h" +#include "util/Text.h" +#include "vm/BooleanObject.h" +#include "vm/DateObject.h" +#include "vm/DateTime.h" +#include "vm/ErrorObject.h" +#include "vm/GlobalObject.h" +#include "vm/HelperThreads.h" +#include "vm/HelperThreadState.h" +#include "vm/Interpreter.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/NumberObject.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseSlot_* +#include "vm/ProxyObject.h" +#include "vm/SavedStacks.h" +#include "vm/ScopeKind.h" +#include "vm/Stack.h" +#include "vm/StencilObject.h" // StencilObject, StencilXDRBufferObject +#include "vm/StringObject.h" +#include "vm/StringType.h" +#include "wasm/AsmJS.h" +#include "wasm/WasmBaselineCompile.h" +#include "wasm/WasmInstance.h" +#include "wasm/WasmIntrinsic.h" +#include "wasm/WasmIonCompile.h" +#include "wasm/WasmJS.h" +#include "wasm/WasmModule.h" +#include "wasm/WasmValType.h" +#include "wasm/WasmValue.h" + +#include "debugger/DebugAPI-inl.h" +#include "vm/Compartment-inl.h" +#include "vm/EnvironmentObject-inl.h" +#include "vm/JSContext-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" +#include "vm/ObjectFlags-inl.h" +#include "vm/StringType-inl.h" +#include "wasm/WasmInstance-inl.h" + +using namespace js; + +using mozilla::AssertedCast; +using mozilla::AsWritableChars; +using mozilla::Maybe; +using mozilla::Span; + +using JS::AutoStableStringChars; +using JS::CompileOptions; +using JS::SourceOwnership; +using JS::SourceText; + +// If fuzzingSafe is set, remove functionality that could cause problems with +// fuzzers. Set this via the environment variable MOZ_FUZZING_SAFE. +mozilla::Atomic<bool> js::fuzzingSafe(false); + +// If disableOOMFunctions is set, disable functionality that causes artificial +// OOM conditions. +static mozilla::Atomic<bool> disableOOMFunctions(false); + +static bool EnvVarIsDefined(const char* name) { + const char* value = getenv(name); + return value && *value; +} + +#if defined(DEBUG) || defined(JS_OOM_BREAKPOINT) +static bool EnvVarAsInt(const char* name, int* valueOut) { + if (!EnvVarIsDefined(name)) { + return false; + } + + *valueOut = atoi(getenv(name)); + return true; +} +#endif + +static bool GetRealmConfiguration(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject info(cx, JS_NewPlainObject(cx)); + if (!info) { + return false; + } + + bool importAssertions = cx->options().importAssertions(); + if (!JS_SetProperty(cx, info, "importAssertions", + importAssertions ? TrueHandleValue : FalseHandleValue)) { + return false; + } + +#ifdef NIGHTLY_BUILD + bool arrayGrouping = cx->realm()->creationOptions().getArrayGroupingEnabled(); + if (!JS_SetProperty(cx, info, "enableArrayGrouping", + arrayGrouping ? TrueHandleValue : FalseHandleValue)) { + return false; + } +#endif + + bool changeArrayByCopy = + cx->realm()->creationOptions().getChangeArrayByCopyEnabled(); + if (!JS_SetProperty(cx, info, "enableChangeArrayByCopy", + changeArrayByCopy ? TrueHandleValue : FalseHandleValue)) { + return false; + } + +#ifdef ENABLE_NEW_SET_METHODS + bool newSetMethods = cx->realm()->creationOptions().getNewSetMethodsEnabled(); + if (!JS_SetProperty(cx, info, "enableNewSetMethods", + newSetMethods ? TrueHandleValue : FalseHandleValue)) { + return false; + } +#endif + + args.rval().setObject(*info); + return true; +} + +static bool GetBuildConfiguration(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject info(cx, JS_NewPlainObject(cx)); + if (!info) { + return false; + } + + if (!JS_SetProperty(cx, info, "rooting-analysis", FalseHandleValue)) { + return false; + } + + if (!JS_SetProperty(cx, info, "exact-rooting", TrueHandleValue)) { + return false; + } + + if (!JS_SetProperty(cx, info, "trace-jscalls-api", FalseHandleValue)) { + return false; + } + + if (!JS_SetProperty(cx, info, "incremental-gc", TrueHandleValue)) { + return false; + } + + if (!JS_SetProperty(cx, info, "generational-gc", TrueHandleValue)) { + return false; + } + + if (!JS_SetProperty(cx, info, "oom-backtraces", FalseHandleValue)) { + return false; + } + + RootedValue value(cx); +#ifdef DEBUG + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "debug", value)) { + return false; + } + +#ifdef RELEASE_OR_BETA + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "release_or_beta", value)) { + return false; + } + +#ifdef EARLY_BETA_OR_EARLIER + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "early_beta_or_earlier", value)) { + return false; + } + +#ifdef MOZ_CODE_COVERAGE + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "coverage", value)) { + return false; + } + +#ifdef JS_HAS_CTYPES + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "has-ctypes", value)) { + return false; + } + +#if defined(_M_IX86) || defined(__i386__) + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "x86", value)) { + return false; + } + +#if defined(_M_X64) || defined(__x86_64__) + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "x64", value)) { + return false; + } + +#ifdef JS_CODEGEN_ARM + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "arm", value)) { + return false; + } + +#ifdef JS_SIMULATOR_ARM + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "arm-simulator", value)) { + return false; + } + +#ifdef ANDROID + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "android", value)) { + return false; + } + +#ifdef XP_WIN + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "windows", value)) { + return false; + } + +#ifdef XP_MACOSX + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "osx", value)) { + return false; + } + +#ifdef JS_CODEGEN_ARM64 + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "arm64", value)) { + return false; + } + +#ifdef JS_SIMULATOR_ARM64 + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "arm64-simulator", value)) { + return false; + } + +#ifdef JS_CODEGEN_MIPS32 + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "mips32", value)) { + return false; + } + +#ifdef JS_CODEGEN_MIPS64 + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "mips64", value)) { + return false; + } + +#ifdef JS_SIMULATOR_MIPS32 + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "mips32-simulator", value)) { + return false; + } + +#ifdef JS_SIMULATOR_MIPS64 + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "mips64-simulator", value)) { + return false; + } + +#ifdef JS_SIMULATOR + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "simulator", value)) { + return false; + } + +#ifdef __wasi__ + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "wasi", value)) { + return false; + } + +#ifdef JS_CODEGEN_LOONG64 + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "loong64", value)) { + return false; + } + +#ifdef JS_SIMULATOR_LOONG64 + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "loong64-simulator", value)) { + return false; + } + +#ifdef JS_CODEGEN_RISCV64 + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "riscv64", value)) { + return false; + } + +#ifdef JS_SIMULATOR_RISCV64 + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "riscv64-simulator", value)) { + return false; + } + +#ifdef MOZ_ASAN + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "asan", value)) { + return false; + } + +#ifdef MOZ_TSAN + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "tsan", value)) { + return false; + } + +#ifdef MOZ_UBSAN + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "ubsan", value)) { + return false; + } + +#ifdef JS_GC_ZEAL + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "has-gczeal", value)) { + return false; + } + +#ifdef MOZ_PROFILING + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "profiling", value)) { + return false; + } + +#ifdef INCLUDE_MOZILLA_DTRACE + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "dtrace", value)) { + return false; + } + +#ifdef MOZ_VALGRIND + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "valgrind", value)) { + return false; + } + +#ifdef JS_HAS_INTL_API + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "intl-api", value)) { + return false; + } + +#if defined(SOLARIS) + value = BooleanValue(false); +#else + value = BooleanValue(true); +#endif + if (!JS_SetProperty(cx, info, "mapped-array-buffer", value)) { + return false; + } + +#ifdef MOZ_MEMORY + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "moz-memory", value)) { + return false; + } + + value.setInt32(sizeof(void*)); + if (!JS_SetProperty(cx, info, "pointer-byte-size", value)) { + return false; + } + +#ifdef ENABLE_NEW_SET_METHODS + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "new-set-methods", value)) { + return false; + } + +#ifdef ENABLE_DECORATORS + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "decorators", value)) { + return false; + } + +#ifdef FUZZING + value = BooleanValue(true); +#else + value = BooleanValue(false); +#endif + if (!JS_SetProperty(cx, info, "fuzzing-defined", value)) { + return false; + } + + args.rval().setObject(*info); + return true; +} + +static bool IsLCovEnabled(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(coverage::IsLCovEnabled()); + return true; +} + +static bool TrialInline(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + + FrameIter iter(cx); + if (iter.done() || !iter.isBaseline() || iter.realm() != cx->realm()) { + return true; + } + + jit::BaselineFrame* frame = iter.abstractFramePtr().asBaselineFrame(); + if (!jit::CanIonCompileScript(cx, frame->script())) { + return true; + } + + return jit::DoTrialInlining(cx, frame); +} + +static bool ReturnStringCopy(JSContext* cx, CallArgs& args, + const char* message) { + JSString* str = JS_NewStringCopyZ(cx, message); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static bool MaybeGC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + JS_MaybeGC(cx); + args.rval().setUndefined(); + return true; +} + +static bool GC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + /* + * If the first argument is 'zone', we collect any zones previously + * scheduled for GC via schedulegc. If the first argument is an object, we + * collect the object's zone (and any other zones scheduled for + * GC). Otherwise, we collect all zones. + */ + bool zone = false; + if (args.length() >= 1) { + Value arg = args[0]; + if (arg.isString()) { + if (!JS_StringEqualsLiteral(cx, arg.toString(), "zone", &zone)) { + return false; + } + } else if (arg.isObject()) { + PrepareZoneForGC(cx, UncheckedUnwrap(&arg.toObject())->zone()); + zone = true; + } + } + + JS::GCOptions options = JS::GCOptions::Normal; + JS::GCReason reason = JS::GCReason::API; + if (args.length() >= 2) { + Value arg = args[1]; + if (arg.isString()) { + bool shrinking = false; + bool last_ditch = false; + if (!JS_StringEqualsLiteral(cx, arg.toString(), "shrinking", + &shrinking)) { + return false; + } + if (!JS_StringEqualsLiteral(cx, arg.toString(), "last-ditch", + &last_ditch)) { + return false; + } + if (shrinking) { + options = JS::GCOptions::Shrink; + } else if (last_ditch) { + options = JS::GCOptions::Shrink; + reason = JS::GCReason::LAST_DITCH; + } + } + } + + size_t preBytes = cx->runtime()->gc.heapSize.bytes(); + + if (zone) { + PrepareForDebugGC(cx->runtime()); + } else { + JS::PrepareForFullGC(cx); + } + + JS::NonIncrementalGC(cx, options, reason); + + char buf[256] = {'\0'}; + if (!js::SupportDifferentialTesting()) { + SprintfLiteral(buf, "before %zu, after %zu\n", preBytes, + cx->runtime()->gc.heapSize.bytes()); + } + return ReturnStringCopy(cx, args, buf); +} + +static bool MinorGC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.get(0) == BooleanValue(true)) { + cx->runtime()->gc.storeBuffer().setAboutToOverflow( + JS::GCReason::FULL_GENERIC_BUFFER); + } + + cx->minorGC(JS::GCReason::API); + args.rval().setUndefined(); + return true; +} + +#define PARAM_NAME_LIST_ENTRY(name, key, writable) " " name +#define GC_PARAMETER_ARGS_LIST FOR_EACH_GC_PARAM(PARAM_NAME_LIST_ENTRY) + +static bool GCParameter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JSString* str = ToString(cx, args.get(0)); + if (!str) { + return false; + } + + UniqueChars name = EncodeLatin1(cx, str); + if (!name) { + return false; + } + + JSGCParamKey param; + bool writable; + if (!GetGCParameterInfo(name.get(), ¶m, &writable)) { + JS_ReportErrorASCII( + cx, "the first argument must be one of:" GC_PARAMETER_ARGS_LIST); + return false; + } + + // Request mode. + if (args.length() == 1) { + uint32_t value = JS_GetGCParameter(cx, param); + args.rval().setNumber(value); + return true; + } + + if (!writable) { + JS_ReportErrorASCII(cx, "Attempt to change read-only parameter %s", + name.get()); + return false; + } + + if (disableOOMFunctions) { + switch (param) { + case JSGC_MAX_BYTES: + case JSGC_MAX_NURSERY_BYTES: + args.rval().setUndefined(); + return true; + default: + break; + } + } + + double d; + if (!ToNumber(cx, args[1], &d)) { + return false; + } + + if (d < 0 || d > UINT32_MAX) { + JS_ReportErrorASCII(cx, "Parameter value out of range"); + return false; + } + + uint32_t value = floor(d); + bool ok = cx->runtime()->gc.setParameter(cx, param, value); + if (!ok) { + JS_ReportErrorASCII(cx, "Parameter value out of range"); + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool RelazifyFunctions(JSContext* cx, unsigned argc, Value* vp) { + // Relazifying functions on GC is usually only done for compartments that are + // not active. To aid fuzzing, this testing function allows us to relazify + // even if the compartment is active. + + CallArgs args = CallArgsFromVp(argc, vp); + + // Disable relazification of all scripts on stack. It is a pervasive + // assumption in the engine that running scripts still have bytecode. + for (AllScriptFramesIter i(cx); !i.done(); ++i) { + i.script()->clearAllowRelazify(); + } + + cx->runtime()->allowRelazificationForTesting = true; + + JS::PrepareForFullGC(cx); + JS::NonIncrementalGC(cx, JS::GCOptions::Shrink, JS::GCReason::API); + + cx->runtime()->allowRelazificationForTesting = false; + + args.rval().setUndefined(); + return true; +} + +static bool IsProxy(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "the function takes exactly one argument"); + return false; + } + if (!args[0].isObject()) { + args.rval().setBoolean(false); + return true; + } + args.rval().setBoolean(args[0].toObject().is<ProxyObject>()); + return true; +} + +static bool WasmIsSupported(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(wasm::HasSupport(cx) && + wasm::AnyCompilerAvailable(cx)); + return true; +} + +static bool WasmIsSupportedByHardware(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(wasm::HasPlatformSupport(cx)); + return true; +} + +static bool WasmDebuggingEnabled(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(wasm::HasSupport(cx) && wasm::BaselineAvailable(cx)); + return true; +} + +static bool WasmStreamingEnabled(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(wasm::StreamingCompilationAvailable(cx)); + return true; +} + +static bool WasmCachingEnabled(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(wasm::CodeCachingAvailable(cx)); + return true; +} + +static bool WasmHugeMemorySupported(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); +#ifdef WASM_SUPPORTS_HUGE_MEMORY + args.rval().setBoolean(true); +#else + args.rval().setBoolean(false); +#endif + return true; +} + +static bool WasmMaxMemoryPages(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() < 1) { + JS_ReportErrorASCII(cx, "not enough arguments"); + return false; + } + if (!args.get(0).isString()) { + JS_ReportErrorASCII(cx, "index type must be a string"); + return false; + } + RootedString s(cx, args.get(0).toString()); + Rooted<JSLinearString*> ls(cx, s->ensureLinear(cx)); + if (!ls) { + return false; + } + if (StringEqualsLiteral(ls, "i32")) { + args.rval().setInt32( + int32_t(wasm::MaxMemoryPages(wasm::IndexType::I32).value())); + return true; + } + if (StringEqualsLiteral(ls, "i64")) { +#ifdef ENABLE_WASM_MEMORY64 + if (wasm::Memory64Available(cx)) { + args.rval().setInt32( + int32_t(wasm::MaxMemoryPages(wasm::IndexType::I64).value())); + return true; + } +#endif + JS_ReportErrorASCII(cx, "memory64 not enabled"); + return false; + } + JS_ReportErrorASCII(cx, "bad index type"); + return false; +} + +static bool WasmThreadsEnabled(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(wasm::ThreadsAvailable(cx)); + return true; +} + +#define WASM_FEATURE(NAME, ...) \ + static bool Wasm##NAME##Enabled(JSContext* cx, unsigned argc, Value* vp) { \ + CallArgs args = CallArgsFromVp(argc, vp); \ + args.rval().setBoolean(wasm::NAME##Available(cx)); \ + return true; \ + } +JS_FOR_WASM_FEATURES(WASM_FEATURE, WASM_FEATURE, WASM_FEATURE); +#undef WASM_FEATURE + +static bool WasmSimdEnabled(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(wasm::SimdAvailable(cx)); + return true; +} + +static bool WasmCompilersPresent(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + char buf[256]; + *buf = 0; + if (wasm::BaselinePlatformSupport()) { + strcat(buf, "baseline"); + } + if (wasm::IonPlatformSupport()) { + if (*buf) { + strcat(buf, ","); + } + strcat(buf, "ion"); + } + + JSString* result = JS_NewStringCopyZ(cx, buf); + if (!result) { + return false; + } + + args.rval().setString(result); + return true; +} + +static bool WasmCompileMode(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // This triplet of predicates will select zero or one baseline compiler and + // zero or one optimizing compiler. + bool baseline = wasm::BaselineAvailable(cx); + bool ion = wasm::IonAvailable(cx); + bool none = !baseline && !ion; + bool tiered = baseline && ion; + + JSStringBuilder result(cx); + if (none && !result.append("none")) { + return false; + } + if (baseline && !result.append("baseline")) { + return false; + } + if (tiered && !result.append("+")) { + return false; + } + if (ion && !result.append("ion")) { + return false; + } + if (JSString* str = result.finishString()) { + args.rval().setString(str); + return true; + } + return false; +} + +static bool WasmBaselineDisabledByFeatures(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + bool isDisabled = false; + JSStringBuilder reason(cx); + if (!wasm::BaselineDisabledByFeatures(cx, &isDisabled, &reason)) { + return false; + } + if (isDisabled) { + JSString* result = reason.finishString(); + if (!result) { + return false; + } + args.rval().setString(result); + } else { + args.rval().setBoolean(false); + } + return true; +} + +static bool WasmIonDisabledByFeatures(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + bool isDisabled = false; + JSStringBuilder reason(cx); + if (!wasm::IonDisabledByFeatures(cx, &isDisabled, &reason)) { + return false; + } + if (isDisabled) { + JSString* result = reason.finishString(); + if (!result) { + return false; + } + args.rval().setString(result); + } else { + args.rval().setBoolean(false); + } + return true; +} + +#ifdef ENABLE_WASM_SIMD +# ifdef DEBUG +static char lastAnalysisResult[1024]; + +namespace js { +namespace wasm { +void ReportSimdAnalysis(const char* data) { + strncpy(lastAnalysisResult, data, sizeof(lastAnalysisResult)); + lastAnalysisResult[sizeof(lastAnalysisResult) - 1] = 0; +} +} // namespace wasm +} // namespace js + +// Unstable API for white-box testing of SIMD optimizations. +// +// Current API: takes no arguments, returns a string describing the last Simd +// simplification applied. + +static bool WasmSimdAnalysis(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + JSString* result = + JS_NewStringCopyZ(cx, *lastAnalysisResult ? lastAnalysisResult : "none"); + if (!result) { + return false; + } + args.rval().setString(result); + *lastAnalysisResult = (char)0; + return true; +} +# endif +#endif + +static bool WasmGlobalFromArrayBuffer(JSContext* cx, unsigned argc, Value* vp) { + if (!wasm::HasSupport(cx)) { + JS_ReportErrorASCII(cx, "wasm support unavailable"); + return false; + } + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 2) { + JS_ReportErrorASCII(cx, "not enough arguments"); + return false; + } + + // Get the type of the value + wasm::ValType valType; + if (!wasm::ToValType(cx, args.get(0), &valType)) { + return false; + } + + // Get the array buffer for the value + if (!args.get(1).isObject() || + !args.get(1).toObject().is<ArrayBufferObject>()) { + JS_ReportErrorASCII(cx, "argument is not an array buffer"); + return false; + } + RootedArrayBufferObject buffer( + cx, &args.get(1).toObject().as<ArrayBufferObject>()); + + // Only allow POD to be created from bytes + switch (valType.kind()) { + case wasm::ValType::I32: + case wasm::ValType::I64: + case wasm::ValType::F32: + case wasm::ValType::F64: + case wasm::ValType::V128: + break; + default: + JS_ReportErrorASCII( + cx, "invalid valtype for creating WebAssembly.Global from bytes"); + return false; + } + + // Check we have all the bytes we need + if (valType.size() != buffer->byteLength()) { + JS_ReportErrorASCII(cx, "array buffer has incorrect size"); + return false; + } + + // Copy the bytes from buffer into a tagged val + wasm::RootedVal val(cx); + val.get().initFromRootedLocation(valType, buffer->dataPointer()); + + // Create the global object + RootedObject proto( + cx, GlobalObject::getOrCreatePrototype(cx, JSProto_WasmGlobal)); + if (!proto) { + return false; + } + Rooted<WasmGlobalObject*> result( + cx, WasmGlobalObject::create(cx, val, false, proto)); + if (!result) { + return false; + } + + args.rval().setObject(*result.get()); + return true; +} + +enum class LaneInterp { + I32x4, + I64x2, + F32x4, + F64x2, +}; + +size_t LaneInterpLanes(LaneInterp interp) { + switch (interp) { + case LaneInterp::I32x4: + return 4; + case LaneInterp::I64x2: + return 2; + case LaneInterp::F32x4: + return 4; + case LaneInterp::F64x2: + return 2; + default: + MOZ_ASSERT_UNREACHABLE(); + return 0; + } +} + +static bool ToLaneInterp(JSContext* cx, HandleValue v, LaneInterp* out) { + RootedString interpStr(cx, ToString(cx, v)); + if (!interpStr) { + return false; + } + Rooted<JSLinearString*> interpLinearStr(cx, interpStr->ensureLinear(cx)); + if (!interpLinearStr) { + return false; + } + + if (StringEqualsLiteral(interpLinearStr, "i32x4")) { + *out = LaneInterp::I32x4; + return true; + } else if (StringEqualsLiteral(interpLinearStr, "i64x2")) { + *out = LaneInterp::I64x2; + return true; + } else if (StringEqualsLiteral(interpLinearStr, "f32x4")) { + *out = LaneInterp::F32x4; + return true; + } else if (StringEqualsLiteral(interpLinearStr, "f64x2")) { + *out = LaneInterp::F64x2; + return true; + } + + JS_ReportErrorASCII(cx, "invalid lane interpretation"); + return false; +} + +static bool WasmGlobalExtractLane(JSContext* cx, unsigned argc, Value* vp) { + if (!wasm::HasSupport(cx)) { + JS_ReportErrorASCII(cx, "wasm support unavailable"); + return false; + } + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 3) { + JS_ReportErrorASCII(cx, "not enough arguments"); + return false; + } + + // Get the global value + if (!args.get(0).isObject() || + !args.get(0).toObject().is<WasmGlobalObject>()) { + JS_ReportErrorASCII(cx, "argument is not wasm value"); + return false; + } + Rooted<WasmGlobalObject*> global( + cx, &args.get(0).toObject().as<WasmGlobalObject>()); + + // Check that we have a v128 value + if (global->type().kind() != wasm::ValType::V128) { + JS_ReportErrorASCII(cx, "global is not a v128 value"); + return false; + } + wasm::V128 v128 = global->val().get().v128(); + + // Get the passed interpretation of lanes + LaneInterp interp; + if (!ToLaneInterp(cx, args.get(1), &interp)) { + return false; + } + + // Get the lane to extract + int32_t lane; + if (!ToInt32(cx, args.get(2), &lane)) { + return false; + } + + // Check that the lane interp is valid + if (lane < 0 || size_t(lane) >= LaneInterpLanes(interp)) { + JS_ReportErrorASCII(cx, "invalid lane for interp"); + return false; + } + + wasm::RootedVal val(cx); + switch (interp) { + case LaneInterp::I32x4: { + uint32_t i; + v128.extractLane<uint32_t>(lane, &i); + val.set(wasm::Val(i)); + break; + } + case LaneInterp::I64x2: { + uint64_t i; + v128.extractLane<uint64_t>(lane, &i); + val.set(wasm::Val(i)); + break; + } + case LaneInterp::F32x4: { + float f; + v128.extractLane<float>(lane, &f); + val.set(wasm::Val(f)); + break; + } + case LaneInterp::F64x2: { + double d; + v128.extractLane<double>(lane, &d); + val.set(wasm::Val(d)); + break; + } + default: + MOZ_ASSERT_UNREACHABLE(); + } + + RootedObject proto( + cx, GlobalObject::getOrCreatePrototype(cx, JSProto_WasmGlobal)); + Rooted<WasmGlobalObject*> result( + cx, WasmGlobalObject::create(cx, val, false, proto)); + args.rval().setObject(*result.get()); + return true; +} + +static bool WasmGlobalsEqual(JSContext* cx, unsigned argc, Value* vp) { + if (!wasm::HasSupport(cx)) { + JS_ReportErrorASCII(cx, "wasm support unavailable"); + return false; + } + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 2) { + JS_ReportErrorASCII(cx, "not enough arguments"); + return false; + } + + if (!args.get(0).isObject() || + !args.get(0).toObject().is<WasmGlobalObject>() || + !args.get(1).isObject() || + !args.get(1).toObject().is<WasmGlobalObject>()) { + JS_ReportErrorASCII(cx, "argument is not wasm value"); + return false; + } + + Rooted<WasmGlobalObject*> a(cx, + &args.get(0).toObject().as<WasmGlobalObject>()); + Rooted<WasmGlobalObject*> b(cx, + &args.get(1).toObject().as<WasmGlobalObject>()); + + if (a->type() != b->type()) { + JS_ReportErrorASCII(cx, "globals are of different type"); + return false; + } + + bool result; + const wasm::Val& aVal = a->val().get(); + const wasm::Val& bVal = b->val().get(); + switch (a->type().kind()) { + case wasm::ValType::I32: { + result = aVal.i32() == bVal.i32(); + break; + } + case wasm::ValType::I64: { + result = aVal.i64() == bVal.i64(); + break; + } + case wasm::ValType::F32: { + result = mozilla::BitwiseCast<uint32_t>(aVal.f32()) == + mozilla::BitwiseCast<uint32_t>(bVal.f32()); + break; + } + case wasm::ValType::F64: { + result = mozilla::BitwiseCast<uint64_t>(aVal.f64()) == + mozilla::BitwiseCast<uint64_t>(bVal.f64()); + break; + } + case wasm::ValType::V128: { + // Don't know the interpretation of the v128, so we only can do an exact + // bitwise equality. Testing code can use wasmGlobalExtractLane to + // workaround this if needed. + result = aVal.v128() == bVal.v128(); + break; + } + case wasm::ValType::Ref: { + result = aVal.ref() == bVal.ref(); + break; + } + default: + JS_ReportErrorASCII(cx, "unsupported type"); + return false; + } + args.rval().setBoolean(result); + return true; +} + +// Flavors of NaN values for WebAssembly. +// See +// https://webassembly.github.io/spec/core/syntax/values.html#floating-point. +enum class NaNFlavor { + // A canonical NaN value. + // - the sign bit is unspecified, + // - the 8-bit exponent is set to all 1s + // - the MSB of the payload is set to 1 (a quieted NaN) and all others to 0. + Canonical, + // An arithmetic NaN. This is the same as a canonical NaN including that the + // payload MSB is set to 1, but one or more of the remaining payload bits MAY + // BE set to 1 (a canonical NaN specifies all 0s). + Arithmetic, +}; + +static bool IsNaNFlavor(uint32_t bits, NaNFlavor flavor) { + switch (flavor) { + case NaNFlavor::Canonical: { + return (bits & 0x7fffffff) == 0x7fc00000; + } + case NaNFlavor::Arithmetic: { + const uint32_t ArithmeticNaN = 0x7f800000; + const uint32_t ArithmeticPayloadMSB = 0x00400000; + bool isNaN = (bits & ArithmeticNaN) == ArithmeticNaN; + bool isMSBSet = (bits & ArithmeticPayloadMSB) == ArithmeticPayloadMSB; + return isNaN && isMSBSet; + } + default: + MOZ_CRASH(); + } +} + +static bool IsNaNFlavor(uint64_t bits, NaNFlavor flavor) { + switch (flavor) { + case NaNFlavor::Canonical: { + return (bits & 0x7fffffffffffffff) == 0x7ff8000000000000; + } + case NaNFlavor::Arithmetic: { + uint64_t ArithmeticNaN = 0x7ff0000000000000; + uint64_t ArithmeticPayloadMSB = 0x0008000000000000; + bool isNaN = (bits & ArithmeticNaN) == ArithmeticNaN; + bool isMsbSet = (bits & ArithmeticPayloadMSB) == ArithmeticPayloadMSB; + return isNaN && isMsbSet; + } + default: + MOZ_CRASH(); + } +} + +static bool ToNaNFlavor(JSContext* cx, HandleValue v, NaNFlavor* out) { + RootedString flavorStr(cx, ToString(cx, v)); + if (!flavorStr) { + return false; + } + Rooted<JSLinearString*> flavorLinearStr(cx, flavorStr->ensureLinear(cx)); + if (!flavorLinearStr) { + return false; + } + + if (StringEqualsLiteral(flavorLinearStr, "canonical_nan")) { + *out = NaNFlavor::Canonical; + return true; + } else if (StringEqualsLiteral(flavorLinearStr, "arithmetic_nan")) { + *out = NaNFlavor::Arithmetic; + return true; + } + + JS_ReportErrorASCII(cx, "invalid nan flavor"); + return false; +} + +static bool WasmGlobalIsNaN(JSContext* cx, unsigned argc, Value* vp) { + if (!wasm::HasSupport(cx)) { + JS_ReportErrorASCII(cx, "wasm support unavailable"); + return false; + } + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 2) { + JS_ReportErrorASCII(cx, "not enough arguments"); + return false; + } + + if (!args.get(0).isObject() || + !args.get(0).toObject().is<WasmGlobalObject>()) { + JS_ReportErrorASCII(cx, "argument is not wasm value"); + return false; + } + Rooted<WasmGlobalObject*> global( + cx, &args.get(0).toObject().as<WasmGlobalObject>()); + + NaNFlavor flavor; + if (!ToNaNFlavor(cx, args.get(1), &flavor)) { + return false; + } + + bool result; + const wasm::Val& val = global->val().get(); + switch (global->type().kind()) { + case wasm::ValType::F32: { + result = IsNaNFlavor(mozilla::BitwiseCast<uint32_t>(val.f32()), flavor); + break; + } + case wasm::ValType::F64: { + result = IsNaNFlavor(mozilla::BitwiseCast<uint64_t>(val.f64()), flavor); + break; + } + default: + JS_ReportErrorASCII(cx, "global is not a floating point value"); + return false; + } + args.rval().setBoolean(result); + return true; +} + +static bool WasmGlobalToString(JSContext* cx, unsigned argc, Value* vp) { + if (!wasm::HasSupport(cx)) { + JS_ReportErrorASCII(cx, "wasm support unavailable"); + return false; + } + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 1) { + JS_ReportErrorASCII(cx, "not enough arguments"); + return false; + } + if (!args.get(0).isObject() || + !args.get(0).toObject().is<WasmGlobalObject>()) { + JS_ReportErrorASCII(cx, "argument is not wasm value"); + return false; + } + Rooted<WasmGlobalObject*> global( + cx, &args.get(0).toObject().as<WasmGlobalObject>()); + const wasm::Val& globalVal = global->val().get(); + + UniqueChars result; + switch (globalVal.type().kind()) { + case wasm::ValType::I32: { + result = JS_smprintf("i32:%" PRIx32, globalVal.i32()); + break; + } + case wasm::ValType::I64: { + result = JS_smprintf("i64:%" PRIx64, globalVal.i64()); + break; + } + case wasm::ValType::F32: { + result = JS_smprintf("f32:%f", globalVal.f32()); + break; + } + case wasm::ValType::F64: { + result = JS_smprintf("f64:%lf", globalVal.f64()); + break; + } + case wasm::ValType::V128: { + wasm::V128 v128 = globalVal.v128(); + result = JS_smprintf( + "v128:%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x", v128.bytes[0], + v128.bytes[1], v128.bytes[2], v128.bytes[3], v128.bytes[4], + v128.bytes[5], v128.bytes[6], v128.bytes[7], v128.bytes[8], + v128.bytes[9], v128.bytes[10], v128.bytes[11], v128.bytes[12], + v128.bytes[13], v128.bytes[14], v128.bytes[15]); + break; + } + case wasm::ValType::Ref: { + result = JS_smprintf("ref:%p", globalVal.ref().asJSObject()); + break; + } + default: + MOZ_ASSERT_UNREACHABLE(); + } + + args.rval().setString(JS_NewStringCopyZ(cx, result.get())); + return true; +} + +static bool WasmLosslessInvoke(JSContext* cx, unsigned argc, Value* vp) { + if (!wasm::HasSupport(cx)) { + JS_ReportErrorASCII(cx, "wasm support unavailable"); + return false; + } + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() < 1) { + JS_ReportErrorASCII(cx, "not enough arguments"); + return false; + } + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "argument is not an object"); + return false; + } + + RootedFunction func(cx, args[0].toObject().maybeUnwrapIf<JSFunction>()); + if (!func || !wasm::IsWasmExportedFunction(func)) { + JS_ReportErrorASCII(cx, "argument is not an exported wasm function"); + return false; + } + + // Get the instance and funcIndex for calling the function + wasm::Instance& instance = wasm::ExportedFunctionToInstance(func); + uint32_t funcIndex = wasm::ExportedFunctionToFuncIndex(func); + + // Set up a modified call frame following the standard JS + // [callee, this, arguments...] convention. + RootedValueVector wasmCallFrame(cx); + size_t len = 2 + args.length(); + if (!wasmCallFrame.resize(len)) { + return false; + } + wasmCallFrame[0].set(args.calleev()); + wasmCallFrame[1].set(args.thisv()); + // Copy over the arguments needed to invoke the provided wasm function, + // skipping the wasm function we're calling that is at `args.get(0)`. + for (size_t i = 1; i < args.length(); i++) { + size_t wasmArg = i - 1; + wasmCallFrame[2 + wasmArg].set(args.get(i)); + } + size_t wasmArgc = argc - 1; + CallArgs wasmCallArgs(CallArgsFromVp(wasmArgc, wasmCallFrame.begin())); + + // Invoke the function with the new call frame + bool result = instance.callExport(cx, funcIndex, wasmCallArgs, + wasm::CoercionLevel::Lossless); + // Assign the wasm rval to our rval + args.rval().set(wasmCallArgs.rval()); + return result; +} + +static bool ConvertToTier(JSContext* cx, HandleValue value, + const wasm::Code& code, wasm::Tier* tier) { + RootedString option(cx, JS::ToString(cx, value)); + + if (!option) { + return false; + } + + bool stableTier = false; + bool bestTier = false; + bool baselineTier = false; + bool ionTier = false; + + if (!JS_StringEqualsLiteral(cx, option, "stable", &stableTier) || + !JS_StringEqualsLiteral(cx, option, "best", &bestTier) || + !JS_StringEqualsLiteral(cx, option, "baseline", &baselineTier) || + !JS_StringEqualsLiteral(cx, option, "ion", &ionTier)) { + return false; + } + + if (stableTier) { + *tier = code.stableTier(); + } else if (bestTier) { + *tier = code.bestTier(); + } else if (baselineTier) { + *tier = wasm::Tier::Baseline; + } else if (ionTier) { + *tier = wasm::Tier::Optimized; + } else { + // You can omit the argument but you can't pass just anything you like + return false; + } + + return true; +} + +static bool WasmExtractCode(JSContext* cx, unsigned argc, Value* vp) { + if (!wasm::HasSupport(cx)) { + JS_ReportErrorASCII(cx, "wasm support unavailable"); + return false; + } + + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "argument is not an object"); + return false; + } + + Rooted<WasmModuleObject*> module( + cx, args[0].toObject().maybeUnwrapIf<WasmModuleObject>()); + if (!module) { + JS_ReportErrorASCII(cx, "argument is not a WebAssembly.Module"); + return false; + } + + wasm::Tier tier = module->module().code().stableTier(); + ; + if (args.length() > 1 && + !ConvertToTier(cx, args[1], module->module().code(), &tier)) { + args.rval().setNull(); + return false; + } + + RootedValue result(cx); + if (!module->module().extractCode(cx, tier, &result)) { + return false; + } + + args.rval().set(result); + return true; +} + +struct DisasmBuffer { + JSStringBuilder builder; + bool oom; + explicit DisasmBuffer(JSContext* cx) : builder(cx), oom(false) {} +}; + +static bool HasDisassembler(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(jit::HasDisassembler()); + return true; +} + +MOZ_THREAD_LOCAL(DisasmBuffer*) disasmBuf; + +static void captureDisasmText(const char* text) { + DisasmBuffer* buf = disasmBuf.get(); + if (!buf->builder.append(text, strlen(text)) || !buf->builder.append('\n')) { + buf->oom = true; + } +} + +static bool DisassembleNative(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + + if (args.length() < 1) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_MORE_ARGS_NEEDED, "disnative", "1", "", + "0"); + return false; + } + + if (!args[0].isObject() || !args[0].toObject().is<JSFunction>()) { + JS_ReportErrorASCII(cx, "The first argument must be a function."); + return false; + } + + Sprinter sprinter(cx); + if (!sprinter.init()) { + return false; + } + + RootedFunction fun(cx, &args[0].toObject().as<JSFunction>()); + + uint8_t* jit_begin = nullptr; + uint8_t* jit_end = nullptr; + + if (fun->isAsmJSNative() || fun->isWasmWithJitEntry()) { + if (fun->isAsmJSNative() && !sprinter.jsprintf("; backend=asmjs\n")) { + return false; + } + if (!sprinter.jsprintf("; backend=wasm\n")) { + return false; + } + + js::wasm::Instance& inst = fun->wasmInstance(); + const js::wasm::Code& code = inst.code(); + js::wasm::Tier tier = code.bestTier(); + + const js::wasm::MetadataTier& meta = inst.metadata(tier); + + const js::wasm::CodeSegment& segment = code.segment(tier); + const uint32_t funcIndex = code.getFuncIndex(&*fun); + const js::wasm::FuncExport& func = meta.lookupFuncExport(funcIndex); + const js::wasm::CodeRange& codeRange = meta.codeRange(func); + + jit_begin = segment.base() + codeRange.begin(); + jit_end = segment.base() + codeRange.end(); + } else if (fun->hasJitScript()) { + JSScript* script = fun->nonLazyScript(); + if (script == nullptr) { + return false; + } + + js::jit::IonScript* ion = + script->hasIonScript() ? script->ionScript() : nullptr; + js::jit::BaselineScript* baseline = + script->hasBaselineScript() ? script->baselineScript() : nullptr; + if (ion && ion->method()) { + if (!sprinter.jsprintf("; backend=ion\n")) { + return false; + } + + jit_begin = ion->method()->raw(); + jit_end = ion->method()->rawEnd(); + } else if (baseline) { + if (!sprinter.jsprintf("; backend=baseline\n")) { + return false; + } + + jit_begin = baseline->method()->raw(); + jit_end = baseline->method()->rawEnd(); + } + } else { + return false; + } + + if (jit_begin == nullptr || jit_end == nullptr) { + return false; + } + + DisasmBuffer buf(cx); + disasmBuf.set(&buf); + auto onFinish = mozilla::MakeScopeExit([&] { disasmBuf.set(nullptr); }); + + jit::Disassemble(jit_begin, jit_end - jit_begin, &captureDisasmText); + + if (buf.oom) { + ReportOutOfMemory(cx); + return false; + } + JSString* sresult = buf.builder.finishString(); + if (!sresult) { + ReportOutOfMemory(cx); + return false; + } + sprinter.putString(sresult); + + if (args.length() > 1 && args[1].isString()) { + RootedString str(cx, args[1].toString()); + JS::UniqueChars fileNameBytes = JS_EncodeStringToUTF8(cx, str); + + const char* fileName = fileNameBytes.get(); + if (!fileName) { + ReportOutOfMemory(cx); + return false; + } + + FILE* f = fopen(fileName, "w"); + if (!f) { + JS_ReportErrorASCII(cx, "Could not open file for writing."); + return false; + } + + uintptr_t expected_length = reinterpret_cast<uintptr_t>(jit_end) - + reinterpret_cast<uintptr_t>(jit_begin); + if (expected_length != fwrite(jit_begin, jit_end - jit_begin, 1, f)) { + JS_ReportErrorASCII(cx, "Did not write all function bytes to the file."); + fclose(f); + return false; + } + fclose(f); + } + + JSString* str = JS_NewStringCopyZ(cx, sprinter.string()); + if (!str) { + return false; + } + + args[0].setUndefined(); + args.rval().setString(str); + + return true; +} + +static bool ComputeTier(JSContext* cx, const wasm::Code& code, + HandleValue tierSelection, wasm::Tier* tier) { + *tier = code.stableTier(); + if (!tierSelection.isUndefined() && + !ConvertToTier(cx, tierSelection, code, tier)) { + JS_ReportErrorASCII(cx, "invalid tier"); + return false; + } + + if (!code.hasTier(*tier)) { + JS_ReportErrorASCII(cx, "function missing selected tier"); + return false; + } + + return true; +} + +template <typename DisasmFunction> +static bool DisassembleIt(JSContext* cx, bool asString, MutableHandleValue rval, + DisasmFunction&& disassembleIt) { + if (asString) { + DisasmBuffer buf(cx); + disasmBuf.set(&buf); + auto onFinish = mozilla::MakeScopeExit([&] { disasmBuf.set(nullptr); }); + disassembleIt(captureDisasmText); + if (buf.oom) { + ReportOutOfMemory(cx); + return false; + } + JSString* sresult = buf.builder.finishString(); + if (!sresult) { + ReportOutOfMemory(cx); + return false; + } + rval.setString(sresult); + return true; + } + + disassembleIt([](const char* text) { fprintf(stderr, "%s\n", text); }); + return true; +} + +static bool WasmDisassembleFunction(JSContext* cx, const HandleFunction& func, + HandleValue tierSelection, bool asString, + MutableHandleValue rval) { + wasm::Instance& instance = wasm::ExportedFunctionToInstance(func); + wasm::Tier tier; + + if (!ComputeTier(cx, instance.code(), tierSelection, &tier)) { + return false; + } + + uint32_t funcIndex = wasm::ExportedFunctionToFuncIndex(func); + return DisassembleIt( + cx, asString, rval, [&](void (*captureText)(const char*)) { + instance.disassembleExport(cx, funcIndex, tier, captureText); + }); +} + +static bool WasmDisassembleCode(JSContext* cx, const wasm::Code& code, + HandleValue tierSelection, int kindSelection, + bool asString, MutableHandleValue rval) { + wasm::Tier tier; + if (!ComputeTier(cx, code, tierSelection, &tier)) { + return false; + } + + return DisassembleIt(cx, asString, rval, + [&](void (*captureText)(const char*)) { + code.disassemble(cx, tier, kindSelection, captureText); + }); +} + +static bool WasmDisassemble(JSContext* cx, unsigned argc, Value* vp) { + if (!wasm::HasSupport(cx)) { + JS_ReportErrorASCII(cx, "wasm support unavailable"); + return false; + } + + CallArgs args = CallArgsFromVp(argc, vp); + + args.rval().set(UndefinedValue()); + + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "argument is not an object"); + return false; + } + + bool asString = false; + RootedValue tierSelection(cx); + int kindSelection = (1 << wasm::CodeRange::Function); + if (args.length() > 1 && args[1].isObject()) { + RootedObject options(cx, &args[1].toObject()); + RootedValue val(cx); + + if (!JS_GetProperty(cx, options, "asString", &val)) { + return false; + } + asString = val.isBoolean() && val.toBoolean(); + + if (!JS_GetProperty(cx, options, "tier", &tierSelection)) { + return false; + } + + if (!JS_GetProperty(cx, options, "kinds", &val)) { + return false; + } + if (val.isString() && val.toString()->hasLatin1Chars()) { + AutoStableStringChars stable(cx); + if (!stable.init(cx, val.toString())) { + return false; + } + const char* p = (const char*)(stable.latin1Chars()); + const char* end = p + val.toString()->length(); + int selection = 0; + for (;;) { + if (strncmp(p, "Function", 8) == 0) { + selection |= (1 << wasm::CodeRange::Function); + p += 8; + } else if (strncmp(p, "InterpEntry", 11) == 0) { + selection |= (1 << wasm::CodeRange::InterpEntry); + p += 11; + } else if (strncmp(p, "JitEntry", 8) == 0) { + selection |= (1 << wasm::CodeRange::JitEntry); + p += 8; + } else if (strncmp(p, "ImportInterpExit", 16) == 0) { + selection |= (1 << wasm::CodeRange::ImportInterpExit); + p += 16; + } else if (strncmp(p, "ImportJitExit", 13) == 0) { + selection |= (1 << wasm::CodeRange::ImportJitExit); + p += 13; + } else if (strncmp(p, "all", 3) == 0) { + selection = ~0; + p += 3; + } else { + break; + } + if (p == end || *p != ',') { + break; + } + p++; + } + if (p == end) { + kindSelection = selection; + } else { + JS_ReportErrorASCII(cx, "argument object has invalid `kinds`"); + return false; + } + } + } + + RootedFunction func(cx, args[0].toObject().maybeUnwrapIf<JSFunction>()); + if (func && wasm::IsWasmExportedFunction(func)) { + return WasmDisassembleFunction(cx, func, tierSelection, asString, + args.rval()); + } + if (args[0].toObject().is<WasmModuleObject>()) { + return WasmDisassembleCode( + cx, args[0].toObject().as<WasmModuleObject>().module().code(), + tierSelection, kindSelection, asString, args.rval()); + } + if (args[0].toObject().is<WasmInstanceObject>()) { + return WasmDisassembleCode( + cx, args[0].toObject().as<WasmInstanceObject>().instance().code(), + tierSelection, kindSelection, asString, args.rval()); + } + JS_ReportErrorASCII( + cx, "argument is not an exported wasm function or a wasm module"); + return false; +} + +enum class Flag { Tier2Complete, Deserialized }; + +static bool WasmReturnFlag(JSContext* cx, unsigned argc, Value* vp, Flag flag) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "argument is not an object"); + return false; + } + + Rooted<WasmModuleObject*> module( + cx, args[0].toObject().maybeUnwrapIf<WasmModuleObject>()); + if (!module) { + JS_ReportErrorASCII(cx, "argument is not a WebAssembly.Module"); + return false; + } + + bool b; + switch (flag) { + case Flag::Tier2Complete: + b = !module->module().testingTier2Active(); + break; + case Flag::Deserialized: + b = module->module().loggingDeserialized(); + break; + } + + args.rval().set(BooleanValue(b)); + return true; +} + +static bool WasmHasTier2CompilationCompleted(JSContext* cx, unsigned argc, + Value* vp) { + return WasmReturnFlag(cx, argc, vp, Flag::Tier2Complete); +} + +static bool WasmLoadedFromCache(JSContext* cx, unsigned argc, Value* vp) { + return WasmReturnFlag(cx, argc, vp, Flag::Deserialized); +} + +static bool WasmIntrinsicI8VecMul(JSContext* cx, unsigned argc, Value* vp) { + if (!wasm::HasSupport(cx)) { + JS_ReportErrorASCII(cx, "wasm support unavailable"); + return false; + } + + CallArgs args = CallArgsFromVp(argc, vp); + + wasm::IntrinsicId ids[] = {wasm::IntrinsicId::I8VecMul}; + Rooted<WasmModuleObject*> module(cx); + if (!wasm::CompileIntrinsicModule(cx, ids, wasm::Shareable::False, &module)) { + return false; + } + args.rval().set(ObjectValue(*module.get())); + return true; +} + +static bool LargeArrayBufferSupported(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(ArrayBufferObject::MaxByteLength > + ArrayBufferObject::MaxByteLengthForSmallBuffer); + return true; +} + +static bool IsLazyFunction(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "The function takes exactly one argument."); + return false; + } + if (!args[0].isObject() || !args[0].toObject().is<JSFunction>()) { + JS_ReportErrorASCII(cx, "The first argument should be a function."); + return false; + } + JSFunction* fun = &args[0].toObject().as<JSFunction>(); + args.rval().setBoolean(fun->isInterpreted() && !fun->hasBytecode()); + return true; +} + +static bool IsRelazifiableFunction(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "The function takes exactly one argument."); + return false; + } + if (!args[0].isObject() || !args[0].toObject().is<JSFunction>()) { + JS_ReportErrorASCII(cx, "The first argument should be a function."); + return false; + } + + JSFunction* fun = &args[0].toObject().as<JSFunction>(); + args.rval().setBoolean(fun->hasBytecode() && + fun->nonLazyScript()->allowRelazify()); + return true; +} + +static bool IsInStencilCache(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "The function takes exactly one argument."); + return false; + } + + if (!args[0].isObject() || !args[0].toObject().is<JSFunction>()) { + JS_ReportErrorASCII(cx, "The first argument should be a function."); + return false; + } + + if (fuzzingSafe) { + // When running code concurrently to fill-up the stencil cache, the content + // is not garanteed to be present. + args.rval().setBoolean(false); + return true; + } + + JSFunction* fun = &args[0].toObject().as<JSFunction>(); + BaseScript* script = fun->baseScript(); + RefPtr<ScriptSource> ss = script->scriptSource(); + StencilCache& cache = cx->runtime()->caches().delazificationCache; + auto guard = cache.isSourceCached(ss); + if (!guard) { + args.rval().setBoolean(false); + return true; + } + + StencilContext key(ss, script->extent()); + frontend::CompilationStencil* stencil = cache.lookup(guard, key); + args.rval().setBoolean(bool(stencil)); + return true; +} + +static bool WaitForStencilCache(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "The function takes exactly one argument."); + return false; + } + + if (!args[0].isObject() || !args[0].toObject().is<JSFunction>()) { + JS_ReportErrorASCII(cx, "The first argument should be a function."); + return false; + } + args.rval().setUndefined(); + + JSFunction* fun = &args[0].toObject().as<JSFunction>(); + BaseScript* script = fun->baseScript(); + RefPtr<ScriptSource> ss = script->scriptSource(); + StencilCache& cache = cx->runtime()->caches().delazificationCache; + StencilContext key(ss, script->extent()); + + AutoLockHelperThreadState lock; + if (!HelperThreadState().isInitialized(lock)) { + return true; + } + + while (true) { + { + // This capture a Mutex that we have to release before using the wait + // function. + auto guard = cache.isSourceCached(ss); + if (!guard) { + return true; + } + + frontend::CompilationStencil* stencil = cache.lookup(guard, key); + if (stencil) { + break; + } + } + + HelperThreadState().wait(lock); + } + return true; +} + +static bool HasSameBytecodeData(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 2) { + JS_ReportErrorASCII(cx, "The function takes exactly two argument."); + return false; + } + + auto GetSharedData = [](JSContext* cx, + HandleValue v) -> SharedImmutableScriptData* { + if (!v.isObject()) { + JS_ReportErrorASCII(cx, "The arguments must be interpreted functions."); + return nullptr; + } + + RootedObject obj(cx, CheckedUnwrapDynamic(&v.toObject(), cx)); + if (!obj) { + return nullptr; + } + + if (!obj->is<JSFunction>() || !obj->as<JSFunction>().isInterpreted()) { + JS_ReportErrorASCII(cx, "The arguments must be interpreted functions."); + return nullptr; + } + + AutoRealm ar(cx, obj); + RootedFunction fun(cx, &obj->as<JSFunction>()); + RootedScript script(cx, JSFunction::getOrCreateScript(cx, fun)); + if (!script) { + return nullptr; + } + + MOZ_ASSERT(script->sharedData()); + return script->sharedData(); + }; + + // NOTE: We use RefPtr below to keep the data alive across possible GC since + // the functions may be in different Zones. + + RefPtr<SharedImmutableScriptData> sharedData1 = GetSharedData(cx, args[0]); + if (!sharedData1) { + return false; + } + + RefPtr<SharedImmutableScriptData> sharedData2 = GetSharedData(cx, args[1]); + if (!sharedData2) { + return false; + } + + args.rval().setBoolean(sharedData1 == sharedData2); + return true; +} + +static bool InternalConst(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() == 0) { + JS_ReportErrorASCII(cx, "the function takes exactly one argument"); + return false; + } + + JSString* str = ToString(cx, args[0]); + if (!str) { + return false; + } + JSLinearString* linear = JS_EnsureLinearString(cx, str); + if (!linear) { + return false; + } + + if (JS_LinearStringEqualsLiteral(linear, "MARK_STACK_BASE_CAPACITY")) { + args.rval().setNumber(uint32_t(js::MARK_STACK_BASE_CAPACITY)); + } else { + JS_ReportErrorASCII(cx, "unknown const name"); + return false; + } + return true; +} + +static bool GCPreserveCode(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 0) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + cx->runtime()->gc.setAlwaysPreserveCode(); + + args.rval().setUndefined(); + return true; +} + +#ifdef JS_GC_ZEAL + +static bool ParseGCZealMode(JSContext* cx, const CallArgs& args, + uint8_t* zeal) { + uint32_t value; + if (!ToUint32(cx, args.get(0), &value)) { + return false; + } + + if (value > uint32_t(gc::ZealMode::Limit)) { + JS_ReportErrorASCII(cx, "gczeal argument out of range"); + return false; + } + + *zeal = static_cast<uint8_t>(value); + return true; +} + +static bool GCZeal(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() > 2) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Too many arguments"); + return false; + } + + uint8_t zeal; + if (!ParseGCZealMode(cx, args, &zeal)) { + return false; + } + + uint32_t frequency = JS_DEFAULT_ZEAL_FREQ; + if (args.length() >= 2) { + if (!ToUint32(cx, args.get(1), &frequency)) { + return false; + } + } + + JS_SetGCZeal(cx, zeal, frequency); + args.rval().setUndefined(); + return true; +} + +static bool UnsetGCZeal(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() > 1) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Too many arguments"); + return false; + } + + uint8_t zeal; + if (!ParseGCZealMode(cx, args, &zeal)) { + return false; + } + + JS_UnsetGCZeal(cx, zeal); + args.rval().setUndefined(); + return true; +} + +static bool ScheduleGC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() > 1) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Too many arguments"); + return false; + } + + if (args.length() == 0) { + /* Fetch next zeal trigger only. */ + } else if (args[0].isNumber()) { + /* Schedule a GC to happen after |arg| allocations. */ + JS_ScheduleGC(cx, std::max(int(args[0].toNumber()), 0)); + } else { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Bad argument - expecting number"); + return false; + } + + uint32_t zealBits; + uint32_t freq; + uint32_t next; + JS_GetGCZealBits(cx, &zealBits, &freq, &next); + args.rval().setInt32(next); + return true; +} + +static bool SelectForGC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + /* + * The selectedForMarking set is intended to be manually marked at slice + * start to detect missing pre-barriers. It is invalid for nursery things + * to be in the set, so evict the nursery before adding items. + */ + cx->runtime()->gc.evictNursery(); + + for (unsigned i = 0; i < args.length(); i++) { + if (args[i].isObject()) { + if (!cx->runtime()->gc.selectForMarking(&args[i].toObject())) { + return false; + } + } + } + + args.rval().setUndefined(); + return true; +} + +static bool VerifyPreBarriers(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() > 0) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Too many arguments"); + return false; + } + + gc::VerifyBarriers(cx->runtime(), gc::PreBarrierVerifier); + args.rval().setUndefined(); + return true; +} + +static bool VerifyPostBarriers(JSContext* cx, unsigned argc, Value* vp) { + // This is a no-op since the post barrier verifier was removed. + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length()) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Too many arguments"); + return false; + } + args.rval().setUndefined(); + return true; +} + +static bool CurrentGC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 0) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Too many arguments"); + return false; + } + + RootedObject result(cx, JS_NewPlainObject(cx)); + if (!result) { + return false; + } + + js::gc::GCRuntime& gc = cx->runtime()->gc; + const char* state = StateName(gc.state()); + + RootedString str(cx, JS_NewStringCopyZ(cx, state)); + if (!str) { + return false; + } + RootedValue val(cx, StringValue(str)); + if (!JS_DefineProperty(cx, result, "incrementalState", val, + JSPROP_ENUMERATE)) { + return false; + } + + if (gc.state() == js::gc::State::Sweep) { + val = Int32Value(gc.getCurrentSweepGroupIndex()); + if (!JS_DefineProperty(cx, result, "sweepGroup", val, JSPROP_ENUMERATE)) { + return false; + } + } + + val = BooleanValue(gc.isIncrementalGCInProgress() && gc.isShrinkingGC()); + if (!JS_DefineProperty(cx, result, "isShrinking", val, JSPROP_ENUMERATE)) { + return false; + } + + val = Int32Value(gc.gcNumber()); + if (!JS_DefineProperty(cx, result, "number", val, JSPROP_ENUMERATE)) { + return false; + } + + val = Int32Value(gc.minorGCCount()); + if (!JS_DefineProperty(cx, result, "minorCount", val, JSPROP_ENUMERATE)) { + return false; + } + + val = Int32Value(gc.majorGCCount()); + if (!JS_DefineProperty(cx, result, "majorCount", val, JSPROP_ENUMERATE)) { + return false; + } + + val = BooleanValue(gc.isFullGc()); + if (!JS_DefineProperty(cx, result, "isFull", val, JSPROP_ENUMERATE)) { + return false; + } + + val = BooleanValue(gc.isCompactingGc()); + if (!JS_DefineProperty(cx, result, "isCompacting", val, JSPROP_ENUMERATE)) { + return false; + } + +# ifdef DEBUG + val = Int32Value(gc.testMarkQueuePos()); + if (!JS_DefineProperty(cx, result, "queuePos", val, JSPROP_ENUMERATE)) { + return false; + } +# endif + + args.rval().setObject(*result); + return true; +} + +static bool DeterministicGC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + cx->runtime()->gc.setDeterministic(ToBoolean(args[0])); + args.rval().setUndefined(); + return true; +} + +static bool DumpGCArenaInfo(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + js::gc::DumpArenaInfo(); + args.rval().setUndefined(); + return true; +} + +static bool SetMarkStackLimit(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + int32_t value; + if (!ToInt32(cx, args[0], &value) || value <= 0) { + JS_ReportErrorASCII(cx, "Bad argument to SetMarkStackLimit"); + return false; + } + + if (JS::IsIncrementalGCInProgress(cx)) { + JS_ReportErrorASCII( + cx, "Attempt to set markStackLimit while a GC is in progress"); + return false; + } + + JSRuntime* runtime = cx->runtime(); + AutoLockGC lock(runtime); + runtime->gc.setMarkStackLimit(value, lock); + args.rval().setUndefined(); + return true; +} + +#endif /* JS_GC_ZEAL */ + +static bool SetMallocMaxDirtyPageModifier(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + constexpr int32_t MinSupportedValue = -5; + constexpr int32_t MaxSupportedValue = 16; + + int32_t value; + if (!ToInt32(cx, args[0], &value)) { + return false; + } + if (value < MinSupportedValue || value > MaxSupportedValue) { + JS_ReportErrorASCII(cx, "Bad argument to setMallocMaxDirtyPageModifier"); + return false; + } + + moz_set_max_dirty_page_modifier(value); + + args.rval().setUndefined(); + return true; +} + +static bool GCState(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() > 1) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Too many arguments"); + return false; + } + + const char* state; + + if (args.length() == 1) { + if (!args[0].isObject()) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Expected object"); + return false; + } + + JSObject* obj = UncheckedUnwrap(&args[0].toObject()); + state = gc::StateName(obj->zone()->gcState()); + } else { + state = gc::StateName(cx->runtime()->gc.state()); + } + + return ReturnStringCopy(cx, args, state); +} + +static bool ScheduleZoneForGC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Expecting a single argument"); + return false; + } + + if (args[0].isObject()) { + // Ensure that |zone| is collected during the next GC. + Zone* zone = UncheckedUnwrap(&args[0].toObject())->zone(); + PrepareZoneForGC(cx, zone); + } else if (args[0].isString()) { + // This allows us to schedule the atoms zone for GC. + Zone* zone = args[0].toString()->zoneFromAnyThread(); + if (!CurrentThreadCanAccessZone(zone)) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Specified zone not accessible for GC"); + return false; + } + PrepareZoneForGC(cx, zone); + } else { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, + "Bad argument - expecting object or string"); + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool StartGC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() > 2) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + auto budget = SliceBudget::unlimited(); + if (args.length() >= 1) { + uint32_t work = 0; + if (!ToUint32(cx, args[0], &work)) { + return false; + } + budget = SliceBudget(WorkBudget(work)); + } + + bool shrinking = false; + if (args.length() >= 2) { + Value arg = args[1]; + if (arg.isString()) { + if (!JS_StringEqualsLiteral(cx, arg.toString(), "shrinking", + &shrinking)) { + return false; + } + } + } + + JSRuntime* rt = cx->runtime(); + if (rt->gc.isIncrementalGCInProgress()) { + RootedObject callee(cx, &args.callee()); + JS_ReportErrorASCII(cx, "Incremental GC already in progress"); + return false; + } + + JS::GCOptions options = + shrinking ? JS::GCOptions::Shrink : JS::GCOptions::Normal; + rt->gc.startDebugGC(options, budget); + + args.rval().setUndefined(); + return true; +} + +static bool FinishGC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() > 0) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + JSRuntime* rt = cx->runtime(); + if (rt->gc.isIncrementalGCInProgress()) { + rt->gc.finishGC(JS::GCReason::DEBUG_GC); + } + + args.rval().setUndefined(); + return true; +} + +static bool GCSlice(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() > 2) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + auto budget = SliceBudget::unlimited(); + if (args.length() >= 1) { + uint32_t work = 0; + if (!ToUint32(cx, args[0], &work)) { + return false; + } + budget = SliceBudget(WorkBudget(work)); + } + + bool dontStart = false; + if (args.get(1).isObject()) { + RootedObject options(cx, &args[1].toObject()); + RootedValue v(cx); + if (!JS_GetProperty(cx, options, "dontStart", &v)) { + return false; + } + dontStart = ToBoolean(v); + } + + JSRuntime* rt = cx->runtime(); + if (rt->gc.isIncrementalGCInProgress()) { + rt->gc.debugGCSlice(budget); + } else if (!dontStart) { + rt->gc.startDebugGC(JS::GCOptions::Normal, budget); + } + + args.rval().setUndefined(); + return true; +} + +static bool AbortGC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 0) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + JS::AbortIncrementalGC(cx); + args.rval().setUndefined(); + return true; +} + +static bool FullCompartmentChecks(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + cx->runtime()->gc.setFullCompartmentChecks(ToBoolean(args[0])); + args.rval().setUndefined(); + return true; +} + +static bool NondeterministicGetWeakMapKeys(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + if (!args[0].isObject()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, + "nondeterministicGetWeakMapKeys", "WeakMap", + InformalValueTypeName(args[0])); + return false; + } + RootedObject arr(cx); + RootedObject mapObj(cx, &args[0].toObject()); + if (!JS_NondeterministicGetWeakMapKeys(cx, mapObj, &arr)) { + return false; + } + if (!arr) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_EXPECTED_TYPE, + "nondeterministicGetWeakMapKeys", "WeakMap", + args[0].toObject().getClass()->name); + return false; + } + args.rval().setObject(*arr); + return true; +} + +class HasChildTracer final : public JS::CallbackTracer { + RootedValue child_; + bool found_; + + void onChild(JS::GCCellPtr thing, const char* name) override { + if (thing.asCell() == child_.toGCThing()) { + found_ = true; + } + } + + public: + HasChildTracer(JSContext* cx, HandleValue child) + : JS::CallbackTracer(cx, JS::TracerKind::Callback, + JS::WeakMapTraceAction::TraceKeysAndValues), + child_(cx, child), + found_(false) {} + + bool found() const { return found_; } +}; + +static bool HasChild(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedValue parent(cx, args.get(0)); + RootedValue child(cx, args.get(1)); + + if (!parent.isGCThing() || !child.isGCThing()) { + args.rval().setBoolean(false); + return true; + } + + HasChildTracer trc(cx, child); + TraceChildren(&trc, JS::GCCellPtr(parent.toGCThing(), parent.traceKind())); + args.rval().setBoolean(trc.found()); + return true; +} + +static bool SetSavedStacksRNGState(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "setSavedStacksRNGState", 1)) { + return false; + } + + int32_t seed; + if (!ToInt32(cx, args[0], &seed)) { + return false; + } + + // Either one or the other of the seed arguments must be non-zero; + // make this true no matter what value 'seed' has. + cx->realm()->savedStacks().setRNGState(seed, (seed + 1) * 33); + return true; +} + +static bool GetSavedFrameCount(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setNumber(cx->realm()->savedStacks().count()); + return true; +} + +static bool ClearSavedFrames(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + js::SavedStacks& savedStacks = cx->realm()->savedStacks(); + savedStacks.clear(); + + for (ActivationIterator iter(cx); !iter.done(); ++iter) { + iter->clearLiveSavedFrameCache(); + } + + args.rval().setUndefined(); + return true; +} + +static bool SaveStack(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JS::StackCapture capture((JS::AllFrames())); + if (args.length() >= 1) { + double maxDouble; + if (!ToNumber(cx, args[0], &maxDouble)) { + return false; + } + if (std::isnan(maxDouble) || maxDouble < 0 || maxDouble > UINT32_MAX) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, args[0], + nullptr, "not a valid maximum frame count"); + return false; + } + uint32_t max = uint32_t(maxDouble); + if (max > 0) { + capture = JS::StackCapture(JS::MaxFrames(max)); + } + } + + RootedObject compartmentObject(cx); + if (args.length() >= 2) { + if (!args[1].isObject()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, args[0], + nullptr, "not an object"); + return false; + } + compartmentObject = UncheckedUnwrap(&args[1].toObject()); + if (!compartmentObject) { + return false; + } + } + + RootedObject stack(cx); + { + Maybe<AutoRealm> ar; + if (compartmentObject) { + ar.emplace(cx, compartmentObject); + } + if (!JS::CaptureCurrentStack(cx, &stack, std::move(capture))) { + return false; + } + } + + if (stack && !cx->compartment()->wrap(cx, &stack)) { + return false; + } + + args.rval().setObjectOrNull(stack); + return true; +} + +static bool CaptureFirstSubsumedFrame(JSContext* cx, unsigned argc, + JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "captureFirstSubsumedFrame", 1)) { + return false; + } + + if (!args[0].isObject()) { + JS_ReportErrorASCII(cx, "The argument must be an object"); + return false; + } + + RootedObject obj(cx, &args[0].toObject()); + obj = CheckedUnwrapStatic(obj); + if (!obj) { + JS_ReportErrorASCII(cx, "Denied permission to object."); + return false; + } + + JS::StackCapture capture( + JS::FirstSubsumedFrame(cx, obj->nonCCWRealm()->principals())); + if (args.length() > 1) { + capture.as<JS::FirstSubsumedFrame>().ignoreSelfHosted = + JS::ToBoolean(args[1]); + } + + JS::RootedObject capturedStack(cx); + if (!JS::CaptureCurrentStack(cx, &capturedStack, std::move(capture))) { + return false; + } + + args.rval().setObjectOrNull(capturedStack); + return true; +} + +static bool CallFunctionFromNativeFrame(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "The function takes exactly one argument."); + return false; + } + if (!args[0].isObject() || !IsCallable(args[0])) { + JS_ReportErrorASCII(cx, "The first argument should be a function."); + return false; + } + + RootedObject function(cx, &args[0].toObject()); + return Call(cx, UndefinedHandleValue, function, JS::HandleValueArray::empty(), + args.rval()); +} + +static bool CallFunctionWithAsyncStack(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 3) { + JS_ReportErrorASCII(cx, "The function takes exactly three arguments."); + return false; + } + if (!args[0].isObject() || !IsCallable(args[0])) { + JS_ReportErrorASCII(cx, "The first argument should be a function."); + return false; + } + if (!args[1].isObject() || !args[1].toObject().is<SavedFrame>()) { + JS_ReportErrorASCII(cx, "The second argument should be a SavedFrame."); + return false; + } + if (!args[2].isString() || args[2].toString()->empty()) { + JS_ReportErrorASCII(cx, "The third argument should be a non-empty string."); + return false; + } + + RootedObject function(cx, &args[0].toObject()); + RootedObject stack(cx, &args[1].toObject()); + RootedString asyncCause(cx, args[2].toString()); + UniqueChars utf8Cause = JS_EncodeStringToUTF8(cx, asyncCause); + if (!utf8Cause) { + MOZ_ASSERT(cx->isExceptionPending()); + return false; + } + + JS::AutoSetAsyncStackForNewCalls sas( + cx, stack, utf8Cause.get(), + JS::AutoSetAsyncStackForNewCalls::AsyncCallKind::EXPLICIT); + return Call(cx, UndefinedHandleValue, function, JS::HandleValueArray::empty(), + args.rval()); +} + +static bool EnableTrackAllocations(JSContext* cx, unsigned argc, Value* vp) { + SetAllocationMetadataBuilder(cx, &SavedStacks::metadataBuilder); + return true; +} + +static bool DisableTrackAllocations(JSContext* cx, unsigned argc, Value* vp) { + SetAllocationMetadataBuilder(cx, nullptr); + return true; +} + +static bool SetTestFilenameValidationCallback(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Accept all filenames that start with "safe". In system code also accept + // filenames starting with "system". + auto testCb = [](JSContext* cx, const char* filename) -> bool { + if (strstr(filename, "safe") == filename) { + return true; + } + if (cx->realm()->isSystem() && strstr(filename, "system") == filename) { + return true; + } + + return false; + }; + JS::SetFilenameValidationCallback(testCb); + + args.rval().setUndefined(); + return true; +} + +static JSAtom* GetPropertiesAddedName(JSContext* cx) { + const char* propName = "_propertiesAdded"; + return Atomize(cx, propName, strlen(propName)); +} + +static bool NewObjectWithAddPropertyHook(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + auto addPropHook = [](JSContext* cx, HandleObject obj, HandleId id, + HandleValue v) -> bool { + Rooted<JSAtom*> propName(cx, GetPropertiesAddedName(cx)); + if (!propName) { + return false; + } + // Don't do anything if we're adding the _propertiesAdded property. + RootedId propId(cx, AtomToId(propName)); + if (id == propId) { + return true; + } + // Increment _propertiesAdded. + RootedValue val(cx); + if (!JS_GetPropertyById(cx, obj, propId, &val)) { + return false; + } + if (!val.isInt32() || val.toInt32() == INT32_MAX) { + return true; + } + val.setInt32(val.toInt32() + 1); + return JS_DefinePropertyById(cx, obj, propId, val, 0); + }; + + static const JSClassOps classOps = { + addPropHook, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace + }; + static const JSClass cls = { + "ObjectWithAddPropHook", + 0, + &classOps, + }; + + RootedObject obj(cx, JS_NewObject(cx, &cls)); + if (!obj) { + return false; + } + + // Initialize _propertiesAdded to 0. + Rooted<JSAtom*> propName(cx, GetPropertiesAddedName(cx)); + if (!propName) { + return false; + } + RootedId propId(cx, AtomToId(propName)); + RootedValue val(cx, Int32Value(0)); + if (!JS_DefinePropertyById(cx, obj, propId, val, 0)) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +static bool NewObjectWithCallHook(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + static auto hookShared = [](JSContext* cx, CallArgs& args) { + Rooted<PlainObject*> obj(cx, NewPlainObject(cx)); + if (!obj) { + return false; + } + + // Define |this|. We can't expose the MagicValue to JS, so we use + // "<is_constructing>" in that case. + Rooted<Value> thisv(cx, args.thisv()); + if (thisv.isMagic(JS_IS_CONSTRUCTING)) { + JSString* str = NewStringCopyZ<CanGC>(cx, "<is_constructing>"); + if (!str) { + return false; + } + thisv.setString(str); + } + if (!DefineDataProperty(cx, obj, cx->names().this_, thisv, + JSPROP_ENUMERATE)) { + return false; + } + + // Define |callee|. + if (!DefineDataProperty(cx, obj, cx->names().callee, args.calleev(), + JSPROP_ENUMERATE)) { + return false; + } + + // Define |arguments| array. + Rooted<ArrayObject*> arr( + cx, NewDenseCopiedArray(cx, args.length(), args.array())); + if (!arr) { + return false; + } + Rooted<Value> arrVal(cx, ObjectValue(*arr)); + if (!DefineDataProperty(cx, obj, cx->names().arguments, arrVal, + JSPROP_ENUMERATE)) { + return false; + } + + // Define |newTarget| if constructing. + if (args.isConstructing()) { + const char* propName = "newTarget"; + Rooted<JSAtom*> name(cx, Atomize(cx, propName, strlen(propName))); + if (!name) { + return false; + } + Rooted<PropertyKey> key(cx, NameToId(name->asPropertyName())); + if (!DefineDataProperty(cx, obj, key, args.newTarget(), + JSPROP_ENUMERATE)) { + return false; + } + } + + args.rval().setObject(*obj); + return true; + }; + + static auto callHook = [](JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(!args.isConstructing()); + return hookShared(cx, args); + }; + static auto constructHook = [](JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.isConstructing()); + return hookShared(cx, args); + }; + + static const JSClassOps classOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + callHook, // call + constructHook, // construct + nullptr, // trace + }; + static const JSClass cls = { + "ObjectWithCallHook", + 0, + &classOps, + }; + + Rooted<JSObject*> obj(cx, JS_NewObject(cx, &cls)); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +static constexpr JSClass ObjectWithManyReservedSlotsClass = { + "ObjectWithManyReservedSlots", JSCLASS_HAS_RESERVED_SLOTS(40)}; + +static bool NewObjectWithManyReservedSlots(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + static constexpr size_t NumReservedSlots = + JSCLASS_RESERVED_SLOTS(&ObjectWithManyReservedSlotsClass); + static_assert(NumReservedSlots > NativeObject::MAX_FIXED_SLOTS); + + RootedObject obj(cx, JS_NewObject(cx, &ObjectWithManyReservedSlotsClass)); + if (!obj) { + return false; + } + + for (size_t i = 0; i < NumReservedSlots; i++) { + JS_SetReservedSlot(obj, i, Int32Value(i)); + } + + args.rval().setObject(*obj); + return true; +} + +static bool CheckObjectWithManyReservedSlots(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isObject() || + args[0].toObject().getClass() != &ObjectWithManyReservedSlotsClass) { + JS_ReportErrorASCII(cx, + "Expected object from newObjectWithManyReservedSlots"); + return false; + } + + JSObject* obj = &args[0].toObject(); + + static constexpr size_t NumReservedSlots = + JSCLASS_RESERVED_SLOTS(&ObjectWithManyReservedSlotsClass); + + for (size_t i = 0; i < NumReservedSlots; i++) { + MOZ_RELEASE_ASSERT(JS::GetReservedSlot(obj, i).toInt32() == int32_t(i)); + } + + args.rval().setUndefined(); + return true; +} + +static bool GetWatchtowerLog(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<GCVector<Value>> values(cx, GCVector<Value>(cx)); + + if (auto* log = cx->runtime()->watchtowerTestingLog.ref().get()) { + Rooted<JSObject*> elem(cx); + for (PlainObject* obj : *log) { + elem = obj; + if (!cx->compartment()->wrap(cx, &elem)) { + return false; + } + if (!values.append(ObjectValue(*elem))) { + return false; + } + } + log->clearAndFree(); + } + + ArrayObject* arr = NewDenseCopiedArray(cx, values.length(), values.begin()); + if (!arr) { + return false; + } + + args.rval().setObject(*arr); + return true; +} + +static bool AddWatchtowerTarget(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isObject()) { + JS_ReportErrorASCII(cx, "Expected a single object argument."); + return false; + } + + if (!cx->runtime()->watchtowerTestingLog.ref()) { + auto vec = cx->make_unique<JSRuntime::RootedPlainObjVec>(cx); + if (!vec) { + return false; + } + cx->runtime()->watchtowerTestingLog = std::move(vec); + } + + RootedObject obj(cx, &args[0].toObject()); + if (!JSObject::setUseWatchtowerTestingLog(cx, obj)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +struct TestExternalString : public JSExternalStringCallbacks { + void finalize(char16_t* chars) const override { js_free(chars); } + size_t sizeOfBuffer(const char16_t* chars, + mozilla::MallocSizeOf mallocSizeOf) const override { + return mallocSizeOf(chars); + } +}; + +static constexpr TestExternalString TestExternalStringCallbacks; + +static bool NewString(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString src(cx, ToString(cx, args.get(0))); + if (!src) { + return false; + } + + gc::Heap heap = gc::Heap::Default; + bool wantTwoByte = false; + bool forceExternal = false; + bool maybeExternal = false; + uint32_t capacity = 0; + + if (args.get(1).isObject()) { + RootedObject options(cx, &args[1].toObject()); + RootedValue v(cx); + bool requestTenured = false; + struct BoolSetting { + const char* name; + bool* value; + }; + for (auto [name, setting] : + {BoolSetting{"tenured", &requestTenured}, + BoolSetting{"twoByte", &wantTwoByte}, + BoolSetting{"external", &forceExternal}, + BoolSetting{"maybeExternal", &maybeExternal}}) { + if (!JS_GetProperty(cx, options, name, &v)) { + return false; + } + *setting = ToBoolean(v); // false if not given (or otherwise undefined) + } + struct Uint32Setting { + const char* name; + uint32_t* value; + }; + for (auto [name, setting] : {Uint32Setting{"capacity", &capacity}}) { + if (!JS_GetProperty(cx, options, name, &v)) { + return false; + } + int32_t i32; + if (!ToInt32(cx, v, &i32)) { + return false; + } + if (i32 < 0) { + JS_ReportErrorASCII(cx, "nonnegative value required"); + return false; + } + *setting = static_cast<uint32_t>(i32); + } + + heap = requestTenured ? gc::Heap::Tenured : gc::Heap::Default; + if (forceExternal || maybeExternal) { + wantTwoByte = true; + if (capacity != 0) { + JS_ReportErrorASCII(cx, + "strings cannot be both external and extensible"); + return false; + } + } + } + + auto len = src->length(); + RootedString dest(cx); + + if (forceExternal || maybeExternal) { + auto buf = cx->make_pod_array<char16_t>(len); + if (!buf) { + return false; + } + + if (!JS_CopyStringChars(cx, mozilla::Range<char16_t>(buf.get(), len), + src)) { + return false; + } + + bool isExternal = true; + if (forceExternal) { + dest = JSExternalString::new_(cx, buf.get(), len, + &TestExternalStringCallbacks); + } else { + dest = NewMaybeExternalString( + cx, buf.get(), len, &TestExternalStringCallbacks, &isExternal, heap); + } + if (dest && isExternal) { + (void)buf.release(); // Ownership was transferred. + } + } else { + AutoStableStringChars stable(cx); + if (!wantTwoByte && src->hasLatin1Chars()) { + if (!stable.init(cx, src)) { + return false; + } + } else { + if (!stable.initTwoByte(cx, src)) { + return false; + } + } + if (capacity) { + if (capacity < len) { + capacity = len; + } + if (len == 0) { + JS_ReportErrorASCII(cx, "Cannot set capacity of empty string"); + return false; + } + if (stable.isLatin1()) { + auto news = cx->make_pod_arena_array<JS::Latin1Char>( + js::StringBufferArena, capacity); + if (!news) { + return false; + } + mozilla::PodCopy(news.get(), stable.latin1Chars(), len); + dest = JSLinearString::newValidLength<CanGC>(cx, std::move(news), len, + heap); + } else { + auto news = + cx->make_pod_arena_array<char16_t>(js::StringBufferArena, capacity); + if (!news) { + return false; + } + mozilla::PodCopy(news.get(), stable.twoByteChars(), len); + dest = JSLinearString::newValidLength<CanGC>(cx, std::move(news), len, + heap); + } + if (dest) { + dest->asLinear().makeExtensible(capacity); + } + } else if (wantTwoByte) { + dest = NewStringCopyNDontDeflate<CanGC>(cx, stable.twoByteChars(), len, + heap); + } else if (stable.isLatin1()) { + dest = NewStringCopyN<CanGC>(cx, stable.latin1Chars(), len, heap); + } else { + // Normal behavior: auto-deflate to latin1 if possible. + dest = NewStringCopyN<CanGC>(cx, stable.twoByteChars(), len, heap); + } + } + + if (!dest) { + return false; + } + + args.rval().setString(dest); + return true; +} + +// Warning! This will let you create ropes that I'm not sure would be possible +// otherwise, specifically: +// +// - a rope with a zero-length child +// - a rope that would fit into an inline string +// +static bool NewRope(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.get(0).isString() || !args.get(1).isString()) { + JS_ReportErrorASCII(cx, "newRope requires two string arguments."); + return false; + } + + gc::Heap heap = js::gc::Heap::Default; + if (args.get(2).isObject()) { + RootedObject options(cx, &args[2].toObject()); + RootedValue v(cx); + if (!JS_GetProperty(cx, options, "nursery", &v)) { + return false; + } + if (!v.isUndefined() && !ToBoolean(v)) { + heap = js::gc::Heap::Tenured; + } + } + + RootedString left(cx, args[0].toString()); + RootedString right(cx, args[1].toString()); + size_t length = JS_GetStringLength(left) + JS_GetStringLength(right); + if (length > JSString::MAX_LENGTH) { + JS_ReportErrorASCII(cx, "rope length exceeds maximum string length"); + return false; + } + + // Disallow creating ropes where one side is empty. + if (left->empty() || right->empty()) { + JS_ReportErrorASCII(cx, "rope child mustn't be the empty string"); + return false; + } + + auto* str = JSRope::new_<CanGC>(cx, left, right, length, heap); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static bool IsRope(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.get(0).isString()) { + JS_ReportErrorASCII(cx, "isRope requires a string argument."); + return false; + } + + JSString* str = args[0].toString(); + args.rval().setBoolean(str->isRope()); + return true; +} + +static bool EnsureLinearString(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isString()) { + JS_ReportErrorASCII( + cx, "ensureLinearString takes exactly one string argument."); + return false; + } + + JSLinearString* linear = args[0].toString()->ensureLinear(cx); + if (!linear) { + return false; + } + + args.rval().setString(linear); + return true; +} + +static bool RepresentativeStringArray(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedObject array(cx, JS::NewArrayObject(cx, 0)); + if (!array) { + return false; + } + + if (!JSString::fillWithRepresentatives(cx, array.as<ArrayObject>())) { + return false; + } + + args.rval().setObject(*array); + return true; +} + +#if defined(DEBUG) || defined(JS_OOM_BREAKPOINT) + +static bool OOMThreadTypes(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setInt32(js::THREAD_TYPE_MAX); + return true; +} + +static bool CheckCanSimulateOOM(JSContext* cx) { + if (js::oom::GetThreadType() != js::THREAD_TYPE_MAIN) { + JS_ReportErrorASCII( + cx, "Simulated OOM failure is only supported on the main thread"); + return false; + } + + return true; +} + +static bool SetupOOMFailure(JSContext* cx, bool failAlways, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (disableOOMFunctions) { + args.rval().setUndefined(); + return true; + } + + if (args.length() < 1) { + JS_ReportErrorASCII(cx, "Count argument required"); + return false; + } + + if (args.length() > 2) { + JS_ReportErrorASCII(cx, "Too many arguments"); + return false; + } + + int32_t count; + if (!JS::ToInt32(cx, args.get(0), &count)) { + return false; + } + + if (count <= 0) { + JS_ReportErrorASCII(cx, "OOM cutoff should be positive"); + return false; + } + + uint32_t targetThread = js::THREAD_TYPE_MAIN; + if (args.length() > 1 && !ToUint32(cx, args[1], &targetThread)) { + return false; + } + + if (targetThread == js::THREAD_TYPE_NONE || + targetThread == js::THREAD_TYPE_WORKER || + targetThread >= js::THREAD_TYPE_MAX) { + JS_ReportErrorASCII(cx, "Invalid thread type specified"); + return false; + } + + if (!CheckCanSimulateOOM(cx)) { + return false; + } + + js::oom::simulator.simulateFailureAfter(js::oom::FailureSimulator::Kind::OOM, + count, targetThread, failAlways); + args.rval().setUndefined(); + return true; +} + +static bool OOMAfterAllocations(JSContext* cx, unsigned argc, Value* vp) { + return SetupOOMFailure(cx, true, argc, vp); +} + +static bool OOMAtAllocation(JSContext* cx, unsigned argc, Value* vp) { + return SetupOOMFailure(cx, false, argc, vp); +} + +static bool ResetOOMFailure(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!CheckCanSimulateOOM(cx)) { + return false; + } + + args.rval().setBoolean(js::oom::HadSimulatedOOM()); + js::oom::simulator.reset(); + return true; +} + +static size_t CountCompartments(JSContext* cx) { + size_t count = 0; + for (auto zone : cx->runtime()->gc.zones()) { + count += zone->compartments().length(); + } + return count; +} + +// Iterative failure testing: test a function by simulating failures at indexed +// locations throughout the normal execution path and checking that the +// resulting state of the environment is consistent with the error result. +// +// For example, trigger OOM at every allocation point and test that the function +// either recovers and succeeds or raises an exception and fails. + +class MOZ_STACK_CLASS IterativeFailureTest { + public: + struct FailureSimulator { + virtual void setup(JSContext* cx) {} + virtual void teardown(JSContext* cx) {} + virtual void startSimulating(JSContext* cx, unsigned iteration, + unsigned thread, bool keepFailing) = 0; + virtual bool stopSimulating() = 0; + virtual void cleanup(JSContext* cx) {} + }; + + IterativeFailureTest(JSContext* cx, FailureSimulator& simulator); + bool initParams(const CallArgs& args); + bool test(); + + private: + bool setup(); + bool testThread(unsigned thread); + bool testIteration(unsigned thread, unsigned iteration, + bool& failureWasSimulated, MutableHandleValue exception); + void cleanup(); + void teardown(); + + JSContext* const cx; + FailureSimulator& simulator; + size_t compartmentCount; + + // Test parameters set by initParams. + RootedFunction testFunction; + unsigned threadStart = 0; + unsigned threadEnd = 0; + bool expectExceptionOnFailure = true; + bool keepFailing = false; + bool verbose = false; +}; + +bool RunIterativeFailureTest( + JSContext* cx, const CallArgs& args, + IterativeFailureTest::FailureSimulator& simulator) { + IterativeFailureTest test(cx, simulator); + return test.initParams(args) && test.test(); +} + +IterativeFailureTest::IterativeFailureTest(JSContext* cx, + FailureSimulator& simulator) + : cx(cx), simulator(simulator), testFunction(cx) {} + +bool IterativeFailureTest::test() { + if (disableOOMFunctions) { + return true; + } + + if (!setup()) { + return false; + } + + auto onExit = mozilla::MakeScopeExit([this] { teardown(); }); + + for (unsigned thread = threadStart; thread <= threadEnd; thread++) { + if (!testThread(thread)) { + return false; + } + } + + return true; +} + +bool IterativeFailureTest::setup() { + if (!CheckCanSimulateOOM(cx)) { + return false; + } + + // Disallow nested tests. + if (cx->runningOOMTest) { + JS_ReportErrorASCII( + cx, "Nested call to iterative failure test is not allowed."); + return false; + } + cx->runningOOMTest = true; + + MOZ_ASSERT(!cx->isExceptionPending()); + +# ifdef JS_GC_ZEAL + JS_SetGCZeal(cx, 0, JS_DEFAULT_ZEAL_FREQ); +# endif + + // Delazify the function here if necessary so we don't end up testing that. + if (testFunction->isInterpreted() && + !JSFunction::getOrCreateScript(cx, testFunction)) { + return false; + } + + compartmentCount = CountCompartments(cx); + + simulator.setup(cx); + + return true; +} + +bool IterativeFailureTest::testThread(unsigned thread) { + if (verbose) { + fprintf(stderr, "thread %u\n", thread); + } + + RootedValue exception(cx); + + unsigned iteration = 1; + bool failureWasSimulated; + do { + if (!testIteration(thread, iteration, failureWasSimulated, &exception)) { + return false; + } + + iteration++; + } while (failureWasSimulated); + + if (verbose) { + fprintf(stderr, " finished after %u iterations\n", iteration - 1); + if (!exception.isUndefined()) { + RootedString str(cx, JS::ToString(cx, exception)); + if (!str) { + fprintf(stderr, " error while trying to print exception, giving up\n"); + return false; + } + UniqueChars bytes(JS_EncodeStringToUTF8(cx, str)); + if (!bytes) { + return false; + } + fprintf(stderr, " threw %s\n", bytes.get()); + } + } + + return true; +} + +bool IterativeFailureTest::testIteration(unsigned thread, unsigned iteration, + bool& failureWasSimulated, + MutableHandleValue exception) { + if (verbose) { + fprintf(stderr, " iteration %u\n", iteration); + } + + MOZ_RELEASE_ASSERT(!cx->isExceptionPending()); + + simulator.startSimulating(cx, iteration, thread, keepFailing); + + RootedValue result(cx); + bool ok = JS_CallFunction(cx, cx->global(), testFunction, + HandleValueArray::empty(), &result); + + failureWasSimulated = simulator.stopSimulating(); + + if (ok && cx->isExceptionPending()) { + MOZ_CRASH( + "Thunk execution succeeded but an exception was raised - missing error " + "check?"); + } + + if (!ok && !cx->isExceptionPending() && expectExceptionOnFailure) { + MOZ_CRASH( + "Thunk execution failed but no exception was raised - missing call to " + "js::ReportOutOfMemory()?"); + } + + // Note that it is possible that the function throws an exception unconnected + // to the simulated failure, in which case we ignore it. More correct would be + // to have the caller pass some kind of exception specification and to check + // the exception against it. + if (!failureWasSimulated && cx->isExceptionPending()) { + if (!cx->getPendingException(exception)) { + return false; + } + } + cx->clearPendingException(); + + cleanup(); + + return true; +} + +void IterativeFailureTest::cleanup() { + simulator.cleanup(cx); + + gc::FinishGC(cx); + + // Some tests create a new compartment or zone on every iteration. Our GC is + // triggered by GC allocations and not by number of compartments or zones, so + // these won't normally get cleaned up. The check here stops some tests + // running out of memory. ("Gentlemen, you can't fight in here! This is the + // War oom!") + if (CountCompartments(cx) > compartmentCount + 100) { + JS_GC(cx); + compartmentCount = CountCompartments(cx); + } +} + +void IterativeFailureTest::teardown() { + simulator.teardown(cx); + + cx->runningOOMTest = false; +} + +bool IterativeFailureTest::initParams(const CallArgs& args) { + if (args.length() < 1 || args.length() > 2) { + JS_ReportErrorASCII(cx, "function takes between 1 and 2 arguments."); + return false; + } + + if (!args[0].isObject() || !args[0].toObject().is<JSFunction>()) { + JS_ReportErrorASCII(cx, "The first argument must be the function to test."); + return false; + } + testFunction = &args[0].toObject().as<JSFunction>(); + + if (args.length() == 2) { + if (args[1].isBoolean()) { + expectExceptionOnFailure = args[1].toBoolean(); + } else if (args[1].isObject()) { + RootedObject options(cx, &args[1].toObject()); + RootedValue value(cx); + + if (!JS_GetProperty(cx, options, "expectExceptionOnFailure", &value)) { + return false; + } + if (!value.isUndefined()) { + expectExceptionOnFailure = ToBoolean(value); + } + + if (!JS_GetProperty(cx, options, "keepFailing", &value)) { + return false; + } + if (!value.isUndefined()) { + keepFailing = ToBoolean(value); + } + } else { + JS_ReportErrorASCII( + cx, "The optional second argument must be an object or a boolean."); + return false; + } + } + + // There are some places where we do fail without raising an exception, so + // we can't expose this to the fuzzers by default. + if (fuzzingSafe) { + expectExceptionOnFailure = false; + } + + // Test all threads by default except worker threads. + threadStart = oom::FirstThreadTypeToTest; + threadEnd = oom::LastThreadTypeToTest; + + // Test a single thread type if specified by the OOM_THREAD environment + // variable. + int threadOption = 0; + if (EnvVarAsInt("OOM_THREAD", &threadOption)) { + if (threadOption < oom::FirstThreadTypeToTest || + threadOption > oom::LastThreadTypeToTest) { + JS_ReportErrorASCII(cx, "OOM_THREAD value out of range."); + return false; + } + + threadStart = threadOption; + threadEnd = threadOption; + } + + verbose = EnvVarIsDefined("OOM_VERBOSE"); + + return true; +} + +struct OOMSimulator : public IterativeFailureTest::FailureSimulator { + void setup(JSContext* cx) override { cx->runtime()->hadOutOfMemory = false; } + + void startSimulating(JSContext* cx, unsigned i, unsigned thread, + bool keepFailing) override { + MOZ_ASSERT(!cx->runtime()->hadOutOfMemory); + js::oom::simulator.simulateFailureAfter( + js::oom::FailureSimulator::Kind::OOM, i, thread, keepFailing); + } + + bool stopSimulating() override { + bool handledOOM = js::oom::HadSimulatedOOM(); + js::oom::simulator.reset(); + return handledOOM; + } + + void cleanup(JSContext* cx) override { + cx->runtime()->hadOutOfMemory = false; + } +}; + +static bool OOMTest(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + OOMSimulator simulator; + if (!RunIterativeFailureTest(cx, args, simulator)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +struct StackOOMSimulator : public IterativeFailureTest::FailureSimulator { + void startSimulating(JSContext* cx, unsigned i, unsigned thread, + bool keepFailing) override { + js::oom::simulator.simulateFailureAfter( + js::oom::FailureSimulator::Kind::StackOOM, i, thread, keepFailing); + } + + bool stopSimulating() override { + bool handledOOM = js::oom::HadSimulatedStackOOM(); + js::oom::simulator.reset(); + return handledOOM; + } +}; + +static bool StackTest(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + StackOOMSimulator simulator; + if (!RunIterativeFailureTest(cx, args, simulator)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +struct FailingIterruptSimulator + : public IterativeFailureTest::FailureSimulator { + JSInterruptCallback* prevEnd = nullptr; + + static bool failingInterruptCallback(JSContext* cx) { return false; } + + void setup(JSContext* cx) override { + prevEnd = cx->interruptCallbacks().end(); + JS_AddInterruptCallback(cx, failingInterruptCallback); + } + + void teardown(JSContext* cx) override { + cx->interruptCallbacks().erase(prevEnd, cx->interruptCallbacks().end()); + } + + void startSimulating(JSContext* cx, unsigned i, unsigned thread, + bool keepFailing) override { + js::oom::simulator.simulateFailureAfter( + js::oom::FailureSimulator::Kind::Interrupt, i, thread, keepFailing); + } + + bool stopSimulating() override { + bool handledInterrupt = js::oom::HadSimulatedInterrupt(); + js::oom::simulator.reset(); + return handledInterrupt; + } +}; + +static bool InterruptTest(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + FailingIterruptSimulator simulator; + if (!RunIterativeFailureTest(cx, args, simulator)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +#endif // defined(DEBUG) || defined(JS_OOM_BREAKPOINT) + +static bool SettlePromiseNow(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "settlePromiseNow", 1)) { + return false; + } + if (!args[0].isObject() || !args[0].toObject().is<PromiseObject>()) { + JS_ReportErrorASCII(cx, "first argument must be a Promise object"); + return false; + } + + Rooted<PromiseObject*> promise(cx, &args[0].toObject().as<PromiseObject>()); + if (IsPromiseForAsyncFunctionOrGenerator(promise)) { + JS_ReportErrorASCII( + cx, "async function/generator's promise shouldn't be manually settled"); + return false; + } + + if (promise->state() != JS::PromiseState::Pending) { + JS_ReportErrorASCII(cx, "cannot settle an already-resolved promise"); + return false; + } + + if (IsPromiseWithDefaultResolvingFunction(promise)) { + SetAlreadyResolvedPromiseWithDefaultResolvingFunction(promise); + } + + int32_t flags = promise->flags(); + promise->setFixedSlot( + PromiseSlot_Flags, + Int32Value(flags | PROMISE_FLAG_RESOLVED | PROMISE_FLAG_FULFILLED)); + promise->setFixedSlot(PromiseSlot_ReactionsOrResult, UndefinedValue()); + + DebugAPI::onPromiseSettled(cx, promise); + return true; +} + +static bool GetWaitForAllPromise(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "getWaitForAllPromise", 1)) { + return false; + } + if (!args[0].isObject() || !args[0].toObject().is<ArrayObject>() || + args[0].toObject().as<NativeObject>().isIndexed()) { + JS_ReportErrorASCII( + cx, "first argument must be a dense Array of Promise objects"); + return false; + } + Rooted<NativeObject*> list(cx, &args[0].toObject().as<NativeObject>()); + RootedObjectVector promises(cx); + uint32_t count = list->getDenseInitializedLength(); + if (!promises.resize(count)) { + return false; + } + + for (uint32_t i = 0; i < count; i++) { + RootedValue elem(cx, list->getDenseElement(i)); + if (!elem.isObject() || !elem.toObject().is<PromiseObject>()) { + JS_ReportErrorASCII( + cx, "Each entry in the passed-in Array must be a Promise"); + return false; + } + promises[i].set(&elem.toObject()); + } + + RootedObject resultPromise(cx, JS::GetWaitForAllPromise(cx, promises)); + if (!resultPromise) { + return false; + } + + args.rval().set(ObjectValue(*resultPromise)); + return true; +} + +static bool ResolvePromise(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "resolvePromise", 2)) { + return false; + } + if (!args[0].isObject() || + !UncheckedUnwrap(&args[0].toObject())->is<PromiseObject>()) { + JS_ReportErrorASCII( + cx, "first argument must be a maybe-wrapped Promise object"); + return false; + } + + RootedObject promise(cx, &args[0].toObject()); + RootedValue resolution(cx, args[1]); + mozilla::Maybe<AutoRealm> ar; + if (IsWrapper(promise)) { + promise = UncheckedUnwrap(promise); + ar.emplace(cx, promise); + if (!cx->compartment()->wrap(cx, &resolution)) { + return false; + } + } + + if (IsPromiseForAsyncFunctionOrGenerator(promise)) { + JS_ReportErrorASCII( + cx, + "async function/generator's promise shouldn't be manually resolved"); + return false; + } + + bool result = JS::ResolvePromise(cx, promise, resolution); + if (result) { + args.rval().setUndefined(); + } + return result; +} + +static bool RejectPromise(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "rejectPromise", 2)) { + return false; + } + if (!args[0].isObject() || + !UncheckedUnwrap(&args[0].toObject())->is<PromiseObject>()) { + JS_ReportErrorASCII( + cx, "first argument must be a maybe-wrapped Promise object"); + return false; + } + + RootedObject promise(cx, &args[0].toObject()); + RootedValue reason(cx, args[1]); + mozilla::Maybe<AutoRealm> ar; + if (IsWrapper(promise)) { + promise = UncheckedUnwrap(promise); + ar.emplace(cx, promise); + if (!cx->compartment()->wrap(cx, &reason)) { + return false; + } + } + + if (IsPromiseForAsyncFunctionOrGenerator(promise)) { + JS_ReportErrorASCII( + cx, + "async function/generator's promise shouldn't be manually rejected"); + return false; + } + + bool result = JS::RejectPromise(cx, promise, reason); + if (result) { + args.rval().setUndefined(); + } + return result; +} + +static unsigned finalizeCount = 0; + +static void finalize_counter_finalize(JS::GCContext* gcx, JSObject* obj) { + ++finalizeCount; +} + +static const JSClassOps FinalizeCounterClassOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + finalize_counter_finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +static const JSClass FinalizeCounterClass = { + "FinalizeCounter", JSCLASS_FOREGROUND_FINALIZE, &FinalizeCounterClassOps}; + +static bool MakeFinalizeObserver(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JSObject* obj = + JS_NewObjectWithGivenProto(cx, &FinalizeCounterClass, nullptr); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +static bool FinalizeCount(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setInt32(finalizeCount); + return true; +} + +static bool ResetFinalizeCount(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + finalizeCount = 0; + args.rval().setUndefined(); + return true; +} + +static bool DumpHeap(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + FILE* dumpFile = stdout; + auto closeFile = mozilla::MakeScopeExit([&dumpFile] { + if (dumpFile != stdout) { + fclose(dumpFile); + } + }); + + if (args.length() > 1) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Too many arguments"); + return false; + } + + if (!args.get(0).isUndefined()) { + RootedString str(cx, ToString(cx, args[0])); + if (!str) { + return false; + } + if (!fuzzingSafe) { + UniqueChars fileNameBytes = JS_EncodeStringToUTF8(cx, str); + if (!fileNameBytes) { + return false; + } +#ifdef XP_WIN + UniqueWideChars wideFileNameBytes = + JS::EncodeUtf8ToWide(cx, fileNameBytes.get()); + if (!wideFileNameBytes) { + return false; + } + dumpFile = _wfopen(wideFileNameBytes.get(), L"w"); +#else + UniqueChars narrowFileNameBytes = + JS::EncodeUtf8ToNarrow(cx, fileNameBytes.get()); + if (!narrowFileNameBytes) { + return false; + } + dumpFile = fopen(narrowFileNameBytes.get(), "w"); +#endif + if (!dumpFile) { + JS_ReportErrorUTF8(cx, "can't open %s", fileNameBytes.get()); + return false; + } + } + } + + js::DumpHeap(cx, dumpFile, js::IgnoreNurseryObjects); + + args.rval().setUndefined(); + return true; +} + +static bool Terminate(JSContext* cx, unsigned arg, Value* vp) { + // Print a message to stderr in differential testing to help jsfunfuzz + // find uncatchable-exception bugs. + if (js::SupportDifferentialTesting()) { + fprintf(stderr, "terminate called\n"); + } + + JS_ClearPendingException(cx); + return false; +} + +static bool ReadGeckoProfilingStack(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + + // Return boolean 'false' if profiler is not enabled. + if (!cx->runtime()->geckoProfiler().enabled()) { + args.rval().setBoolean(false); + return true; + } + + // Array holding physical jit stack frames. + RootedObject stack(cx, NewDenseEmptyArray(cx)); + if (!stack) { + return false; + } + + // If profiler sampling has been suppressed, return an empty + // stack. + if (!cx->isProfilerSamplingEnabled()) { + args.rval().setObject(*stack); + return true; + } + + struct InlineFrameInfo { + InlineFrameInfo(const char* kind, UniqueChars label) + : kind(kind), label(std::move(label)) {} + const char* kind; + UniqueChars label; + }; + + Vector<Vector<InlineFrameInfo, 0, TempAllocPolicy>, 0, TempAllocPolicy> + frameInfo(cx); + + JS::ProfilingFrameIterator::RegisterState state; + for (JS::ProfilingFrameIterator i(cx, state); !i.done(); ++i) { + MOZ_ASSERT(i.stackAddress() != nullptr); + + if (!frameInfo.emplaceBack(cx)) { + return false; + } + + const size_t MaxInlineFrames = 16; + JS::ProfilingFrameIterator::Frame frames[MaxInlineFrames]; + uint32_t nframes = i.extractStack(frames, 0, MaxInlineFrames); + MOZ_ASSERT(nframes <= MaxInlineFrames); + for (uint32_t i = 0; i < nframes; i++) { + const char* frameKindStr = nullptr; + switch (frames[i].kind) { + case JS::ProfilingFrameIterator::Frame_BaselineInterpreter: + frameKindStr = "baseline-interpreter"; + break; + case JS::ProfilingFrameIterator::Frame_Baseline: + frameKindStr = "baseline-jit"; + break; + case JS::ProfilingFrameIterator::Frame_Ion: + frameKindStr = "ion"; + break; + case JS::ProfilingFrameIterator::Frame_Wasm: + frameKindStr = "wasm"; + break; + default: + frameKindStr = "unknown"; + } + + UniqueChars label = + DuplicateStringToArena(js::StringBufferArena, cx, frames[i].label); + if (!label) { + return false; + } + + if (!frameInfo.back().emplaceBack(frameKindStr, std::move(label))) { + return false; + } + } + } + + RootedObject inlineFrameInfo(cx); + RootedString frameKind(cx); + RootedString frameLabel(cx); + RootedId idx(cx); + + const unsigned propAttrs = JSPROP_ENUMERATE; + + uint32_t physicalFrameNo = 0; + for (auto& frame : frameInfo) { + // Array holding all inline frames in a single physical jit stack frame. + RootedObject inlineStack(cx, NewDenseEmptyArray(cx)); + if (!inlineStack) { + return false; + } + + uint32_t inlineFrameNo = 0; + for (auto& inlineFrame : frame) { + // Object holding frame info. + RootedObject inlineFrameInfo(cx, NewPlainObject(cx)); + if (!inlineFrameInfo) { + return false; + } + + frameKind = NewStringCopyZ<CanGC>(cx, inlineFrame.kind); + if (!frameKind) { + return false; + } + + if (!JS_DefineProperty(cx, inlineFrameInfo, "kind", frameKind, + propAttrs)) { + return false; + } + + frameLabel = NewLatin1StringZ(cx, std::move(inlineFrame.label)); + if (!frameLabel) { + return false; + } + + if (!JS_DefineProperty(cx, inlineFrameInfo, "label", frameLabel, + propAttrs)) { + return false; + } + + idx = PropertyKey::Int(inlineFrameNo); + if (!JS_DefinePropertyById(cx, inlineStack, idx, inlineFrameInfo, 0)) { + return false; + } + + ++inlineFrameNo; + } + + // Push inline array into main array. + idx = PropertyKey::Int(physicalFrameNo); + if (!JS_DefinePropertyById(cx, stack, idx, inlineStack, 0)) { + return false; + } + + ++physicalFrameNo; + } + + args.rval().setObject(*stack); + return true; +} + +static bool ReadGeckoInterpProfilingStack(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + + // Return boolean 'false' if profiler is not enabled. + if (!cx->runtime()->geckoProfiler().enabled()) { + args.rval().setBoolean(false); + return true; + } + + // Array with information about each frame. + Rooted<JSObject*> stack(cx, NewDenseEmptyArray(cx)); + if (!stack) { + return false; + } + uint32_t stackIndex = 0; + + ProfilingStack* profStack = cx->geckoProfiler().getProfilingStack(); + MOZ_ASSERT(profStack); + + for (size_t i = 0; i < profStack->stackSize(); i++) { + const auto& frame = profStack->frames[i]; + if (!frame.isJsFrame()) { + continue; + } + + // Skip fake JS frame pushed for js::RunScript by GeckoProfilerEntryMarker. + const char* dynamicStr = frame.dynamicString(); + if (!dynamicStr) { + continue; + } + + Rooted<PlainObject*> frameInfo(cx, NewPlainObject(cx)); + if (!frameInfo) { + return false; + } + + Rooted<JSString*> dynamicString( + cx, JS_NewStringCopyUTF8Z( + cx, JS::ConstUTF8CharsZ(dynamicStr, strlen(dynamicStr)))); + if (!dynamicString) { + return false; + } + if (!JS_DefineProperty(cx, frameInfo, "dynamicString", dynamicString, + JSPROP_ENUMERATE)) { + return false; + } + + if (!JS_DefineElement(cx, stack, stackIndex, frameInfo, JSPROP_ENUMERATE)) { + return false; + } + stackIndex++; + } + + args.rval().setObject(*stack); + return true; +} + +static bool EnableOsiPointRegisterChecks(JSContext*, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); +#ifdef CHECK_OSIPOINT_REGISTERS + jit::JitOptions.checkOsiPointRegisters = true; +#endif + args.rval().setUndefined(); + return true; +} + +static bool DisplayName(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.get(0).isObject() || !args[0].toObject().is<JSFunction>()) { + RootedObject arg(cx, &args.callee()); + ReportUsageErrorASCII(cx, arg, "Must have one function argument"); + return false; + } + + JSFunction* fun = &args[0].toObject().as<JSFunction>(); + JSString* str = fun->displayAtom(); + args.rval().setString(str ? str : cx->runtime()->emptyString.ref()); + return true; +} + +static bool IsAvxPresent(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); +#if defined(JS_CODEGEN_X64) || defined(JS_CODEGEN_X86) + int minVersion = 1; + if (argc > 0 && args.get(0).isNumber()) { + minVersion = std::max(1, int(args[0].toNumber())); + } + switch (minVersion) { + case 1: + args.rval().setBoolean(jit::Assembler::HasAVX()); + return true; + case 2: + args.rval().setBoolean(jit::Assembler::HasAVX2()); + return true; + } +#endif + args.rval().setBoolean(false); + return true; +} + +class ShellAllocationMetadataBuilder : public AllocationMetadataBuilder { + public: + ShellAllocationMetadataBuilder() : AllocationMetadataBuilder() {} + + virtual JSObject* build(JSContext* cx, HandleObject, + AutoEnterOOMUnsafeRegion& oomUnsafe) const override; + + static const ShellAllocationMetadataBuilder metadataBuilder; +}; + +JSObject* ShellAllocationMetadataBuilder::build( + JSContext* cx, HandleObject, AutoEnterOOMUnsafeRegion& oomUnsafe) const { + RootedObject obj(cx, NewPlainObject(cx)); + if (!obj) { + oomUnsafe.crash("ShellAllocationMetadataBuilder::build"); + } + + RootedObject stack(cx, NewDenseEmptyArray(cx)); + if (!stack) { + oomUnsafe.crash("ShellAllocationMetadataBuilder::build"); + } + + static int createdIndex = 0; + createdIndex++; + + if (!JS_DefineProperty(cx, obj, "index", createdIndex, 0)) { + oomUnsafe.crash("ShellAllocationMetadataBuilder::build"); + } + + if (!JS_DefineProperty(cx, obj, "stack", stack, 0)) { + oomUnsafe.crash("ShellAllocationMetadataBuilder::build"); + } + + int stackIndex = 0; + RootedId id(cx); + RootedValue callee(cx); + for (NonBuiltinScriptFrameIter iter(cx); !iter.done(); ++iter) { + if (iter.isFunctionFrame() && iter.compartment() == cx->compartment()) { + id = PropertyKey::Int(stackIndex); + RootedObject callee(cx, iter.callee(cx)); + if (!JS_DefinePropertyById(cx, stack, id, callee, JSPROP_ENUMERATE)) { + oomUnsafe.crash("ShellAllocationMetadataBuilder::build"); + } + stackIndex++; + } + } + + return obj; +} + +const ShellAllocationMetadataBuilder + ShellAllocationMetadataBuilder::metadataBuilder; + +static bool EnableShellAllocationMetadataBuilder(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + SetAllocationMetadataBuilder( + cx, &ShellAllocationMetadataBuilder::metadataBuilder); + + args.rval().setUndefined(); + return true; +} + +static bool GetAllocationMetadata(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1 || !args[0].isObject()) { + JS_ReportErrorASCII(cx, "Argument must be an object"); + return false; + } + + args.rval().setObjectOrNull(GetAllocationMetadata(&args[0].toObject())); + return true; +} + +static bool testingFunc_bailout(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // NOP when not in IonMonkey + args.rval().setUndefined(); + return true; +} + +static bool testingFunc_bailAfter(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1 || !args[0].isInt32() || args[0].toInt32() < 0) { + JS_ReportErrorASCII( + cx, "Argument must be a positive number that fits in an int32"); + return false; + } + +#ifdef DEBUG + if (auto* jitRuntime = cx->runtime()->jitRuntime()) { + uint32_t bailAfter = args[0].toInt32(); + bool enableBailAfter = bailAfter > 0; + if (jitRuntime->ionBailAfterEnabled() != enableBailAfter) { + // Force JIT code to be recompiled with (or without) instrumentation. + ReleaseAllJITCode(cx->gcContext()); + jitRuntime->setIonBailAfterEnabled(enableBailAfter); + } + jitRuntime->setIonBailAfterCounter(bailAfter); + } +#endif + + args.rval().setUndefined(); + return true; +} + +static bool testingFunc_invalidate(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // If the topmost frame is Ion/Warp, find the IonScript and invalidate it. + FrameIter iter(cx); + if (!iter.done() && iter.isIon()) { + while (!iter.isPhysicalJitFrame()) { + ++iter; + } + if (iter.script()->hasIonScript()) { + js::jit::Invalidate(cx, iter.script()); + } + } + + args.rval().setUndefined(); + return true; +} + +static constexpr unsigned JitWarmupResetLimit = 20; +static_assert(JitWarmupResetLimit <= + unsigned(JSScript::MutableFlags::WarmupResets_MASK), + "JitWarmupResetLimit exceeds max value"); + +static bool testingFunc_inJit(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!jit::IsBaselineJitEnabled(cx)) { + return ReturnStringCopy(cx, args, "Baseline is disabled."); + } + + // Use frame iterator to inspect caller. + FrameIter iter(cx); + + // We may be invoked directly, not in a JS context, e.g. if inJit is added as + // a callback on the event queue. + if (iter.done()) { + args.rval().setBoolean(false); + return true; + } + + if (iter.hasScript()) { + // Detect repeated attempts to compile, resetting the counter if inJit + // succeeds. Note: This script may have be inlined into its caller. + if (iter.isJSJit()) { + iter.script()->resetWarmUpResetCounter(); + } else if (iter.script()->getWarmUpResetCount() >= JitWarmupResetLimit) { + return ReturnStringCopy( + cx, args, "Compilation is being repeatedly prevented. Giving up."); + } + } + + // Returns true for any JIT (including WASM). + MOZ_ASSERT_IF(iter.isJSJit(), cx->currentlyRunningInJit()); + args.rval().setBoolean(cx->currentlyRunningInJit()); + return true; +} + +static bool testingFunc_inIon(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!jit::IsIonEnabled(cx)) { + return ReturnStringCopy(cx, args, "Ion is disabled."); + } + + // Use frame iterator to inspect caller. + FrameIter iter(cx); + + // We may be invoked directly, not in a JS context, e.g. if inJson is added as + // a callback on the event queue. + if (iter.done()) { + args.rval().setBoolean(false); + return true; + } + + if (iter.hasScript()) { + // Detect repeated attempts to compile, resetting the counter if inIon + // succeeds. Note: This script may have be inlined into its caller. + if (iter.isIon()) { + iter.script()->resetWarmUpResetCounter(); + } else if (!iter.script()->canIonCompile()) { + return ReturnStringCopy(cx, args, "Unable to Ion-compile this script."); + } else if (iter.script()->getWarmUpResetCount() >= JitWarmupResetLimit) { + return ReturnStringCopy( + cx, args, "Compilation is being repeatedly prevented. Giving up."); + } + } + + args.rval().setBoolean(iter.isIon()); + return true; +} + +bool js::testingFunc_assertFloat32(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 2) { + JS_ReportErrorASCII(cx, "Expects only 2 arguments"); + return false; + } + + // NOP when not in IonMonkey + args.rval().setUndefined(); + return true; +} + +static bool TestingFunc_assertJitStackInvariants(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + jit::AssertJitStackInvariants(cx); + args.rval().setUndefined(); + return true; +} + +bool js::testingFunc_assertRecoveredOnBailout(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 2) { + JS_ReportErrorASCII(cx, "Expects only 2 arguments"); + return false; + } + + // NOP when not in IonMonkey + args.rval().setUndefined(); + return true; +} + +static bool GetJitCompilerOptions(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject info(cx, JS_NewPlainObject(cx)); + if (!info) { + return false; + } + + uint32_t intValue = 0; + RootedValue value(cx); + +#define JIT_COMPILER_MATCH(key, string) \ + opt = JSJITCOMPILER_##key; \ + if (JS_GetGlobalJitCompilerOption(cx, opt, &intValue)) { \ + value.setInt32(intValue); \ + if (!JS_SetProperty(cx, info, string, value)) return false; \ + } + + JSJitCompilerOption opt = JSJITCOMPILER_NOT_AN_OPTION; + JIT_COMPILER_OPTIONS(JIT_COMPILER_MATCH); +#undef JIT_COMPILER_MATCH + + args.rval().setObject(*info); + return true; +} + +static bool SetIonCheckGraphCoherency(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + jit::JitOptions.checkGraphConsistency = ToBoolean(args.get(0)); + args.rval().setUndefined(); + return true; +} + +// A JSObject that holds structured clone data, similar to the C++ class +// JSAutoStructuredCloneBuffer. +class CloneBufferObject : public NativeObject { + static const JSPropertySpec props_[3]; + + static const size_t DATA_SLOT = 0; + static const size_t SYNTHETIC_SLOT = 1; + static const size_t NUM_SLOTS = 2; + + public: + static const JSClass class_; + + static CloneBufferObject* Create(JSContext* cx) { + RootedObject obj(cx, JS_NewObject(cx, &class_)); + if (!obj) { + return nullptr; + } + obj->as<CloneBufferObject>().setReservedSlot(DATA_SLOT, + PrivateValue(nullptr)); + obj->as<CloneBufferObject>().setReservedSlot(SYNTHETIC_SLOT, + BooleanValue(false)); + + if (!JS_DefineProperties(cx, obj, props_)) { + return nullptr; + } + + return &obj->as<CloneBufferObject>(); + } + + static CloneBufferObject* Create(JSContext* cx, + JSAutoStructuredCloneBuffer* buffer) { + Rooted<CloneBufferObject*> obj(cx, Create(cx)); + if (!obj) { + return nullptr; + } + auto data = js::MakeUnique<JSStructuredCloneData>(buffer->scope()); + if (!data) { + ReportOutOfMemory(cx); + return nullptr; + } + buffer->giveTo(data.get()); + obj->setData(data.release(), false); + return obj; + } + + JSStructuredCloneData* data() const { + return static_cast<JSStructuredCloneData*>( + getReservedSlot(DATA_SLOT).toPrivate()); + } + + bool isSynthetic() const { + return getReservedSlot(SYNTHETIC_SLOT).toBoolean(); + } + + void setData(JSStructuredCloneData* aData, bool synthetic) { + MOZ_ASSERT(!data()); + setReservedSlot(DATA_SLOT, PrivateValue(aData)); + setReservedSlot(SYNTHETIC_SLOT, BooleanValue(synthetic)); + } + + // Discard an owned clone buffer. + void discard() { + js_delete(data()); + setReservedSlot(DATA_SLOT, PrivateValue(nullptr)); + } + + static bool setCloneBuffer_impl(JSContext* cx, const CallArgs& args) { + Rooted<CloneBufferObject*> obj( + cx, &args.thisv().toObject().as<CloneBufferObject>()); + + const char* data = nullptr; + UniqueChars dataOwner; + size_t nbytes; + + if (args.get(0).isObject() && args[0].toObject().is<ArrayBufferObject>()) { + ArrayBufferObject* buffer = &args[0].toObject().as<ArrayBufferObject>(); + bool isSharedMemory; + uint8_t* dataBytes = nullptr; + JS::GetArrayBufferLengthAndData(buffer, &nbytes, &isSharedMemory, + &dataBytes); + MOZ_ASSERT(!isSharedMemory); + data = reinterpret_cast<char*>(dataBytes); + } else { + JSString* str = JS::ToString(cx, args.get(0)); + if (!str) { + return false; + } + dataOwner = JS_EncodeStringToLatin1(cx, str); + if (!dataOwner) { + return false; + } + data = dataOwner.get(); + nbytes = JS_GetStringLength(str); + } + + if (nbytes == 0 || (nbytes % sizeof(uint64_t) != 0)) { + JS_ReportErrorASCII(cx, "Invalid length for clonebuffer data"); + return false; + } + + auto buf = js::MakeUnique<JSStructuredCloneData>( + JS::StructuredCloneScope::DifferentProcess); + if (!buf || !buf->Init(nbytes)) { + ReportOutOfMemory(cx); + return false; + } + + MOZ_ALWAYS_TRUE(buf->AppendBytes(data, nbytes)); + obj->discard(); + obj->setData(buf.release(), true); + + args.rval().setUndefined(); + return true; + } + + static bool is(HandleValue v) { + return v.isObject() && v.toObject().is<CloneBufferObject>(); + } + + static bool setCloneBuffer(JSContext* cx, unsigned int argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, setCloneBuffer_impl>(cx, args); + } + + static bool getData(JSContext* cx, Handle<CloneBufferObject*> obj, + JSStructuredCloneData** data) { + if (!obj->data()) { + *data = nullptr; + return true; + } + + bool hasTransferable; + if (!JS_StructuredCloneHasTransferables(*obj->data(), &hasTransferable)) { + return false; + } + + if (hasTransferable) { + JS_ReportErrorASCII( + cx, "cannot retrieve structured clone buffer with transferables"); + return false; + } + + *data = obj->data(); + return true; + } + + static bool getCloneBuffer_impl(JSContext* cx, const CallArgs& args) { + Rooted<CloneBufferObject*> obj( + cx, &args.thisv().toObject().as<CloneBufferObject>()); + MOZ_ASSERT(args.length() == 0); + + JSStructuredCloneData* data; + if (!getData(cx, obj, &data)) { + return false; + } + + size_t size = data->Size(); + UniqueChars buffer(js_pod_malloc<char>(size)); + if (!buffer) { + ReportOutOfMemory(cx); + return false; + } + auto iter = data->Start(); + if (!data->ReadBytes(iter, buffer.get(), size)) { + ReportOutOfMemory(cx); + return false; + } + JSString* str = JS_NewStringCopyN(cx, buffer.get(), size); + if (!str) { + return false; + } + args.rval().setString(str); + return true; + } + + static bool getCloneBuffer(JSContext* cx, unsigned int argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, getCloneBuffer_impl>(cx, args); + } + + static bool getCloneBufferAsArrayBuffer_impl(JSContext* cx, + const CallArgs& args) { + Rooted<CloneBufferObject*> obj( + cx, &args.thisv().toObject().as<CloneBufferObject>()); + MOZ_ASSERT(args.length() == 0); + + JSStructuredCloneData* data; + if (!getData(cx, obj, &data)) { + return false; + } + + size_t size = data->Size(); + UniqueChars buffer(js_pod_malloc<char>(size)); + if (!buffer) { + ReportOutOfMemory(cx); + return false; + } + auto iter = data->Start(); + if (!data->ReadBytes(iter, buffer.get(), size)) { + ReportOutOfMemory(cx); + return false; + } + + auto* rawBuffer = buffer.release(); + JSObject* arrayBuffer = JS::NewArrayBufferWithContents(cx, size, rawBuffer); + if (!arrayBuffer) { + js_free(rawBuffer); + return false; + } + + args.rval().setObject(*arrayBuffer); + return true; + } + + static bool getCloneBufferAsArrayBuffer(JSContext* cx, unsigned int argc, + JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, getCloneBufferAsArrayBuffer_impl>(cx, args); + } + + static void Finalize(JS::GCContext* gcx, JSObject* obj) { + obj->as<CloneBufferObject>().discard(); + } +}; + +static const JSClassOps CloneBufferObjectClassOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + CloneBufferObject::Finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass CloneBufferObject::class_ = { + "CloneBuffer", + JSCLASS_HAS_RESERVED_SLOTS(CloneBufferObject::NUM_SLOTS) | + JSCLASS_FOREGROUND_FINALIZE, + &CloneBufferObjectClassOps}; + +const JSPropertySpec CloneBufferObject::props_[] = { + JS_PSGS("clonebuffer", getCloneBuffer, setCloneBuffer, 0), + JS_PSGS("arraybuffer", getCloneBufferAsArrayBuffer, setCloneBuffer, 0), + JS_PS_END}; + +static mozilla::Maybe<JS::StructuredCloneScope> ParseCloneScope( + JSContext* cx, HandleString str) { + mozilla::Maybe<JS::StructuredCloneScope> scope; + + JSLinearString* scopeStr = str->ensureLinear(cx); + if (!scopeStr) { + return scope; + } + + if (StringEqualsLiteral(scopeStr, "SameProcess")) { + scope.emplace(JS::StructuredCloneScope::SameProcess); + } else if (StringEqualsLiteral(scopeStr, "DifferentProcess")) { + scope.emplace(JS::StructuredCloneScope::DifferentProcess); + } else if (StringEqualsLiteral(scopeStr, "DifferentProcessForIndexedDB")) { + scope.emplace(JS::StructuredCloneScope::DifferentProcessForIndexedDB); + } + + return scope; +} + +// A custom object that is serializable and transferable using +// the engine's custom hooks. The callbacks log their activity +// to a JSRuntime-wide log (tagging actions with IDs to distinguish them). +class CustomSerializableObject : public NativeObject { + static const size_t ID_SLOT = 0; + static const size_t DETACHED_SLOT = 1; + static const size_t BEHAVIOR_SLOT = 2; + static const size_t NUM_SLOTS = 3; + + static constexpr size_t MAX_LOG_LEN = 100; + + // The activity log should be specific to a JSRuntime. + struct ActivityLog { + uint32_t buffer[MAX_LOG_LEN]; + size_t length = 0; + + static MOZ_THREAD_LOCAL(ActivityLog*) self; + static ActivityLog* getThreadLog() { + if (!self.initialized() || !self.get()) { + self.infallibleInit(); + self.set(js_new<ActivityLog>()); + MOZ_RELEASE_ASSERT(self.get()); + } + return self.get(); + } + + static bool log(int32_t id, char action) { + return getThreadLog()->logImpl(id, action); + } + + bool logImpl(int32_t id, char action) { + if (length + 2 > MAX_LOG_LEN) { + return false; + } + buffer[length++] = id; + buffer[length++] = uint32_t(action); + return true; + } + }; + + public: + enum class Behavior { + Nothing = 0, + FailDuringReadTransfer = 1, + FailDuringRead = 2 + }; + + static constexpr JSClass class_ = {"CustomSerializable", + JSCLASS_HAS_RESERVED_SLOTS(NUM_SLOTS)}; + + static bool is(HandleValue v) { + return v.isObject() && v.toObject().is<CustomSerializableObject>(); + } + + static CustomSerializableObject* Create(JSContext* cx, int32_t id, + Behavior behavior) { + Rooted<CustomSerializableObject*> obj( + cx, static_cast<CustomSerializableObject*>(JS_NewObject(cx, &class_))); + if (!obj) { + return nullptr; + } + obj->setReservedSlot(ID_SLOT, Int32Value(id)); + obj->setReservedSlot(DETACHED_SLOT, BooleanValue(false)); + obj->setReservedSlot(BEHAVIOR_SLOT, + Int32Value(static_cast<int32_t>(behavior))); + + if (!JS_DefineProperty(cx, obj, "log", getLog, clearLog, 0)) { + return nullptr; + } + + return obj; + } + + public: + static uint32_t tag() { return JS_SCTAG_USER_MIN; } + + static bool log(int32_t id, char action) { + return ActivityLog::log(id, action); + } + bool log(char action) { + return log(getReservedSlot(ID_SLOT).toInt32(), action); + } + + void detach() { setReservedSlot(DETACHED_SLOT, BooleanValue(true)); } + bool isDetached() { return getReservedSlot(DETACHED_SLOT).toBoolean(); } + + uint32_t id() const { return getReservedSlot(ID_SLOT).toInt32(); } + Behavior behavior() { + return static_cast<Behavior>(getReservedSlot(BEHAVIOR_SLOT).toInt32()); + } + + static bool getLog(JSContext* cx, unsigned int argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<is, getLog_impl>(cx, args); + } + + static bool getLog_impl(JSContext* cx, const CallArgs& args) { + Rooted<CustomSerializableObject*> obj( + cx, &args.thisv().toObject().as<CustomSerializableObject>()); + + size_t len = ActivityLog::getThreadLog()->length; + uint32_t* logBuffer = ActivityLog::getThreadLog()->buffer; + + Rooted<ArrayObject*> result(cx, NewDenseFullyAllocatedArray(cx, len)); + if (!result) { + return false; + } + result->ensureDenseInitializedLength(0, len); + + for (size_t p = 0; p < len; p += 2) { + int32_t id = int32_t(logBuffer[p]); + char action = char(logBuffer[p + 1]); + result->setDenseElement(p, Int32Value(id)); + JSString* str = JS_NewStringCopyN(cx, &action, 1); + if (!str) { + return false; + } + result->setDenseElement(p + 1, StringValue(str)); + } + + args.rval().setObject(*result); + return true; + } + + static bool clearLog(JSContext* cx, unsigned int argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.get(0).isNullOrUndefined()) { + JS_ReportErrorASCII(cx, "log may only be assigned null/undefined"); + return false; + } + ActivityLog::getThreadLog()->length = 0; + args.rval().setUndefined(); + return true; + } + + static bool Write(JSContext* cx, JSStructuredCloneWriter* w, + JS::HandleObject aObj, bool* sameProcessScopeRequired, + void* closure) { + Rooted<CustomSerializableObject*> obj(cx); + + if ((obj = aObj->maybeUnwrapIf<CustomSerializableObject>())) { + obj->log('w'); + // Write a regular clone as a <tag, id> pair, followed by <0, behavior>. + // Note that transferring will communicate the behavior via a different + // mechanism. + return JS_WriteUint32Pair(w, obj->tag(), obj->id()) && + JS_WriteUint32Pair(w, 0, static_cast<uint32_t>(obj->behavior())); + } + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_SC_UNSUPPORTED_TYPE); + return false; + } + + static JSObject* Read(JSContext* cx, JSStructuredCloneReader* r, + const JS::CloneDataPolicy& cloneDataPolicy, + uint32_t tag, uint32_t id, void* closure) { + uint32_t dummy, behaviorData; + if (!JS_ReadUint32Pair(r, &dummy, &behaviorData)) { + return nullptr; + } + if (dummy != 0 || id > INT32_MAX) { + JS_ReportErrorASCII(cx, "out of range"); + return nullptr; + } + + auto b = static_cast<Behavior>(behaviorData); + Rooted<CustomSerializableObject*> obj( + cx, Create(cx, static_cast<int32_t>(id), b)); + if (!obj) { + return nullptr; + } + + obj->log('r'); + if (obj->behavior() == Behavior::FailDuringRead) { + JS_ReportErrorASCII(cx, + "Failed as requested in read during deserialization"); + return nullptr; + } + return obj; + } + + static bool CanTransfer(JSContext* cx, JS::Handle<JSObject*> wrapped, + bool* sameProcessScopeRequired, void* closure) { + Rooted<CustomSerializableObject*> obj(cx); + + if ((obj = wrapped->maybeUnwrapIf<CustomSerializableObject>())) { + obj->log('?'); + // For now, all CustomSerializable objects are considered to be + // transferable. + return true; + } + + return false; + } + + static bool WriteTransfer(JSContext* cx, JS::Handle<JSObject*> aObj, + void* closure, uint32_t* tag, + JS::TransferableOwnership* ownership, + void** content, uint64_t* extraData) { + Rooted<CustomSerializableObject*> obj(cx); + + if ((obj = aObj->maybeUnwrapIf<CustomSerializableObject>())) { + if (obj->isDetached()) { + JS_ReportErrorASCII(cx, "Attempted to transfer detached object"); + return false; + } + obj->log('W'); + *content = reinterpret_cast<void*>(obj->id()); + *extraData = static_cast<uint64_t>(obj->behavior()); + *tag = obj->tag(); + *ownership = JS::SCTAG_TMO_CUSTOM; + obj->detach(); + return true; + } + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_SC_NOT_TRANSFERABLE); + return false; + } + + static bool ReadTransfer(JSContext* cx, JSStructuredCloneReader* r, + uint32_t tag, void* content, uint64_t extraData, + void* closure, + JS::MutableHandleObject returnObject) { + if (tag == CustomSerializableObject::tag()) { + int32_t id = int32_t(reinterpret_cast<uintptr_t>(content)); + Rooted<CustomSerializableObject*> obj( + cx, CustomSerializableObject::Create( + cx, id, static_cast<Behavior>(extraData))); + if (!obj) { + return false; + } + obj->log('R'); + if (obj->behavior() == Behavior::FailDuringReadTransfer) { + return false; + } + returnObject.set(obj); + return true; + } + + return false; + } + + static void FreeTransfer(uint32_t tag, JS::TransferableOwnership ownership, + void* content, uint64_t extraData, void* closure) { + CustomSerializableObject::log(uint32_t(reinterpret_cast<intptr_t>(content)), + 'F'); + } +}; + +MOZ_THREAD_LOCAL(CustomSerializableObject::ActivityLog*) +CustomSerializableObject::ActivityLog::self; + +static bool MakeSerializable(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + int32_t id = 0; + if (args.get(0).isInt32()) { + id = args[0].toInt32(); + if (id < 0) { + JS_ReportErrorASCII(cx, "id out of range"); + return false; + } + } + CustomSerializableObject::Behavior behavior = + CustomSerializableObject::Behavior::Nothing; + if (args.get(1).isInt32()) { + int32_t iv = args[1].toInt32(); + constexpr int32_t min = + static_cast<int32_t>(CustomSerializableObject::Behavior::Nothing); + constexpr int32_t max = static_cast<int32_t>( + CustomSerializableObject::Behavior::FailDuringRead); + if (iv < min || iv > max) { + JS_ReportErrorASCII(cx, "behavior out of range"); + return false; + } + behavior = static_cast<CustomSerializableObject::Behavior>(iv); + } + + JSObject* obj = CustomSerializableObject::Create(cx, id, behavior); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +static JSStructuredCloneCallbacks gCloneCallbacks = { + .read = CustomSerializableObject::Read, + .write = CustomSerializableObject::Write, + .reportError = nullptr, + .readTransfer = CustomSerializableObject::ReadTransfer, + .writeTransfer = CustomSerializableObject::WriteTransfer, + .freeTransfer = CustomSerializableObject::FreeTransfer, + .canTransfer = CustomSerializableObject::CanTransfer, + .sabCloned = nullptr}; + +bool js::testingFunc_serialize(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (js::SupportDifferentialTesting()) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, + "Function unavailable in differential testing mode."); + return false; + } + + mozilla::Maybe<JSAutoStructuredCloneBuffer> clonebuf; + JS::CloneDataPolicy policy; + + if (!args.get(2).isUndefined()) { + RootedObject opts(cx, ToObject(cx, args.get(2))); + if (!opts) { + return false; + } + + RootedValue v(cx); + if (!JS_GetProperty(cx, opts, "SharedArrayBuffer", &v)) { + return false; + } + + if (!v.isUndefined()) { + JSString* str = JS::ToString(cx, v); + if (!str) { + return false; + } + JSLinearString* poli = str->ensureLinear(cx); + if (!poli) { + return false; + } + + if (StringEqualsLiteral(poli, "allow")) { + policy.allowSharedMemoryObjects(); + policy.allowIntraClusterClonableSharedObjects(); + } else if (StringEqualsLiteral(poli, "deny")) { + // default + } else { + JS_ReportErrorASCII(cx, "Invalid policy value for 'SharedArrayBuffer'"); + return false; + } + } + + if (!JS_GetProperty(cx, opts, "scope", &v)) { + return false; + } + + if (!v.isUndefined()) { + RootedString str(cx, JS::ToString(cx, v)); + if (!str) { + return false; + } + auto scope = ParseCloneScope(cx, str); + if (!scope) { + JS_ReportErrorASCII(cx, "Invalid structured clone scope"); + return false; + } + clonebuf.emplace(*scope, &gCloneCallbacks, nullptr); + } + } + + if (!clonebuf) { + clonebuf.emplace(JS::StructuredCloneScope::SameProcess, &gCloneCallbacks, + nullptr); + } + + if (!clonebuf->write(cx, args.get(0), args.get(1), policy)) { + return false; + } + + RootedObject obj(cx, CloneBufferObject::Create(cx, clonebuf.ptr())); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +static bool Deserialize(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (js::SupportDifferentialTesting()) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, + "Function unavailable in differential testing mode."); + return false; + } + + if (!args.get(0).isObject() || !args[0].toObject().is<CloneBufferObject>()) { + JS_ReportErrorASCII(cx, "deserialize requires a clonebuffer argument"); + return false; + } + Rooted<CloneBufferObject*> obj(cx, + &args[0].toObject().as<CloneBufferObject>()); + + JS::CloneDataPolicy policy; + + JS::StructuredCloneScope scope = + obj->isSynthetic() ? JS::StructuredCloneScope::DifferentProcess + : JS::StructuredCloneScope::SameProcess; + if (args.get(1).isObject()) { + RootedObject opts(cx, &args[1].toObject()); + if (!opts) { + return false; + } + + RootedValue v(cx); + if (!JS_GetProperty(cx, opts, "SharedArrayBuffer", &v)) { + return false; + } + + if (!v.isUndefined()) { + JSString* str = JS::ToString(cx, v); + if (!str) { + return false; + } + JSLinearString* poli = str->ensureLinear(cx); + if (!poli) { + return false; + } + + if (StringEqualsLiteral(poli, "allow")) { + policy.allowSharedMemoryObjects(); + policy.allowIntraClusterClonableSharedObjects(); + } else if (StringEqualsLiteral(poli, "deny")) { + // default + } else { + JS_ReportErrorASCII(cx, "Invalid policy value for 'SharedArrayBuffer'"); + return false; + } + } + + if (!JS_GetProperty(cx, opts, "scope", &v)) { + return false; + } + + if (!v.isUndefined()) { + RootedString str(cx, JS::ToString(cx, v)); + if (!str) { + return false; + } + auto maybeScope = ParseCloneScope(cx, str); + if (!maybeScope) { + JS_ReportErrorASCII(cx, "Invalid structured clone scope"); + return false; + } + + if (*maybeScope < scope) { + JS_ReportErrorASCII(cx, + "Cannot use less restrictive scope " + "than the deserialized clone buffer's scope"); + return false; + } + + scope = *maybeScope; + } + } + + // Clone buffer was already consumed? + if (!obj->data()) { + JS_ReportErrorASCII(cx, + "deserialize given invalid clone buffer " + "(transferables already consumed?)"); + return false; + } + + bool hasTransferable; + if (!JS_StructuredCloneHasTransferables(*obj->data(), &hasTransferable)) { + return false; + } + + RootedValue deserialized(cx); + if (!JS_ReadStructuredClone(cx, *obj->data(), JS_STRUCTURED_CLONE_VERSION, + scope, &deserialized, policy, &gCloneCallbacks, + nullptr)) { + return false; + } + args.rval().set(deserialized); + + // Consume any clone buffer with transferables; throw an error if it is + // deserialized again. + if (hasTransferable) { + obj->discard(); + } + + return true; +} + +static bool DetachArrayBuffer(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "detachArrayBuffer() requires a single argument"); + return false; + } + + if (!args[0].isObject()) { + JS_ReportErrorASCII(cx, "detachArrayBuffer must be passed an object"); + return false; + } + + RootedObject obj(cx, &args[0].toObject()); + if (!JS::DetachArrayBuffer(cx, obj)) { + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool HelperThreadCount(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (js::SupportDifferentialTesting()) { + // Always return 0 to get consistent output with and without --no-threads. + args.rval().setInt32(0); + return true; + } + + if (CanUseExtraThreads()) { + args.rval().setInt32(GetHelperThreadCount()); + } else { + args.rval().setInt32(0); + } + return true; +} + +static bool EnableShapeConsistencyChecks(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); +#ifdef DEBUG + NativeObject::enableShapeConsistencyChecks(); +#endif + args.rval().setUndefined(); + return true; +} + +// ShapeSnapshot holds information about an object's properties. This is used +// for checking object and shape changes between two points in time. +class ShapeSnapshot { + HeapPtr<JSObject*> object_; + HeapPtr<Shape*> shape_; + HeapPtr<BaseShape*> baseShape_; + ObjectFlags objectFlags_; + + GCVector<HeapPtr<Value>, 8> slots_; + + struct PropertySnapshot { + HeapPtr<PropMap*> propMap; + uint32_t propMapIndex; + HeapPtr<PropertyKey> key; + PropertyInfo prop; + + explicit PropertySnapshot(PropMap* map, uint32_t index) + : propMap(map), + propMapIndex(index), + key(map->getKey(index)), + prop(map->getPropertyInfo(index)) {} + + void trace(JSTracer* trc) { + TraceEdge(trc, &propMap, "propMap"); + TraceEdge(trc, &key, "key"); + } + + bool operator==(const PropertySnapshot& other) const { + return propMap == other.propMap && propMapIndex == other.propMapIndex && + key == other.key && prop == other.prop; + } + bool operator!=(const PropertySnapshot& other) const { + return !operator==(other); + } + }; + GCVector<PropertySnapshot, 8> properties_; + + public: + explicit ShapeSnapshot(JSContext* cx) : slots_(cx), properties_(cx) {} + void checkSelf(JSContext* cx) const; + void check(JSContext* cx, const ShapeSnapshot& other) const; + bool init(JSObject* obj); + void trace(JSTracer* trc); + + JSObject* object() const { return object_; } +}; + +// A JSObject that holds a ShapeSnapshot. +class ShapeSnapshotObject : public NativeObject { + static constexpr size_t SnapshotSlot = 0; + static constexpr size_t ReservedSlots = 1; + + public: + static const JSClassOps classOps_; + static const JSClass class_; + + bool hasSnapshot() const { + // The snapshot may not be present yet if we GC during initialization. + return !getReservedSlot(SnapshotSlot).isUndefined(); + } + + ShapeSnapshot& snapshot() const { + void* ptr = getReservedSlot(SnapshotSlot).toPrivate(); + MOZ_ASSERT(ptr); + return *static_cast<ShapeSnapshot*>(ptr); + } + + static ShapeSnapshotObject* create(JSContext* cx, HandleObject obj); + + static void finalize(JS::GCContext* gcx, JSObject* obj) { + if (obj->as<ShapeSnapshotObject>().hasSnapshot()) { + js_delete(&obj->as<ShapeSnapshotObject>().snapshot()); + } + } + static void trace(JSTracer* trc, JSObject* obj) { + if (obj->as<ShapeSnapshotObject>().hasSnapshot()) { + obj->as<ShapeSnapshotObject>().snapshot().trace(trc); + } + } +}; + +/*static */ const JSClassOps ShapeSnapshotObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + ShapeSnapshotObject::finalize, // finalize + nullptr, // call + nullptr, // construct + ShapeSnapshotObject::trace, // trace +}; + +/*static */ const JSClass ShapeSnapshotObject::class_ = { + "ShapeSnapshotObject", + JSCLASS_HAS_RESERVED_SLOTS(ShapeSnapshotObject::ReservedSlots) | + JSCLASS_BACKGROUND_FINALIZE, + &ShapeSnapshotObject::classOps_}; + +bool ShapeSnapshot::init(JSObject* obj) { + object_ = obj; + shape_ = obj->shape(); + baseShape_ = shape_->base(); + objectFlags_ = shape_->objectFlags(); + + if (obj->is<NativeObject>()) { + NativeObject* nobj = &obj->as<NativeObject>(); + + // Snapshot the slot values. + size_t slotSpan = nobj->slotSpan(); + if (!slots_.growBy(slotSpan)) { + return false; + } + for (size_t i = 0; i < slotSpan; i++) { + slots_[i] = nobj->getSlot(i); + } + + // Snapshot property information. + if (uint32_t len = nobj->shape()->propMapLength(); len > 0) { + PropMap* map = nobj->shape()->propMap(); + while (true) { + for (uint32_t i = 0; i < len; i++) { + if (!map->hasKey(i)) { + continue; + } + if (!properties_.append(PropertySnapshot(map, i))) { + return false; + } + } + if (!map->hasPrevious()) { + break; + } + map = map->asLinked()->previous(); + len = PropMap::Capacity; + } + } + } + + return true; +} + +void ShapeSnapshot::trace(JSTracer* trc) { + TraceEdge(trc, &object_, "object"); + TraceEdge(trc, &shape_, "shape"); + TraceEdge(trc, &baseShape_, "baseShape"); + slots_.trace(trc); + properties_.trace(trc); +} + +void ShapeSnapshot::checkSelf(JSContext* cx) const { + // Assertions based on a single snapshot. + + // Non-dictionary shapes must not be mutated. + if (!shape_->isDictionary()) { + MOZ_RELEASE_ASSERT(shape_->base() == baseShape_); + MOZ_RELEASE_ASSERT(shape_->objectFlags() == objectFlags_); + } + + for (const PropertySnapshot& propSnapshot : properties_) { + PropMap* propMap = propSnapshot.propMap; + uint32_t propMapIndex = propSnapshot.propMapIndex; + PropertyInfo prop = propSnapshot.prop; + + // Skip if the map no longer matches the snapshotted data. This can + // only happen for non-configurable dictionary properties. + if (PropertySnapshot(propMap, propMapIndex) != propSnapshot) { + MOZ_RELEASE_ASSERT(propMap->isDictionary()); + MOZ_RELEASE_ASSERT(prop.configurable()); + continue; + } + + // Ensure ObjectFlags depending on property information are set if needed. + ObjectFlags expectedFlags = GetObjectFlagsForNewProperty( + shape_->getObjectClass(), shape_->objectFlags(), propSnapshot.key, + prop.flags(), cx); + MOZ_RELEASE_ASSERT(expectedFlags == objectFlags_); + + // Accessors must have a PrivateGCThingValue(GetterSetter*) slot value. + if (prop.isAccessorProperty()) { + Value slotVal = slots_[prop.slot()]; + MOZ_RELEASE_ASSERT(slotVal.isPrivateGCThing()); + MOZ_RELEASE_ASSERT(slotVal.toGCThing()->is<GetterSetter>()); + } + + // Data properties must not have a PrivateGCThingValue slot value. + if (prop.isDataProperty()) { + Value slotVal = slots_[prop.slot()]; + MOZ_RELEASE_ASSERT(!slotVal.isPrivateGCThing()); + } + } +} + +void ShapeSnapshot::check(JSContext* cx, const ShapeSnapshot& later) const { + checkSelf(cx); + later.checkSelf(cx); + + if (object_ != later.object_) { + // Snapshots are for different objects. Assert dictionary shapes aren't + // shared. + if (object_->is<NativeObject>()) { + NativeObject* nobj = &object_->as<NativeObject>(); + if (nobj->inDictionaryMode()) { + MOZ_RELEASE_ASSERT(shape_ != later.shape_); + } + } + return; + } + + // We have two snapshots for the same object. Check the shape information + // wasn't changed in invalid ways. + + // If the Shape is still the same, the object must have the same BaseShape, + // ObjectFlags and property information. + if (shape_ == later.shape_) { + MOZ_RELEASE_ASSERT(objectFlags_ == later.objectFlags_); + MOZ_RELEASE_ASSERT(baseShape_ == later.baseShape_); + MOZ_RELEASE_ASSERT(slots_.length() == later.slots_.length()); + MOZ_RELEASE_ASSERT(properties_.length() == later.properties_.length()); + + for (size_t i = 0; i < properties_.length(); i++) { + MOZ_RELEASE_ASSERT(properties_[i] == later.properties_[i]); + // Non-configurable accessor properties and non-configurable, non-writable + // data properties shouldn't have had their slot mutated. + PropertyInfo prop = properties_[i].prop; + if (!prop.configurable()) { + if (prop.isAccessorProperty() || + (prop.isDataProperty() && !prop.writable())) { + size_t slot = prop.slot(); + MOZ_RELEASE_ASSERT(slots_[slot] == later.slots_[slot]); + } + } + } + } + + // Object flags should not be lost. The exception is the Indexed flag, it + // can be cleared when densifying elements, so clear that flag first. + { + ObjectFlags flags = objectFlags_; + ObjectFlags flagsLater = later.objectFlags_; + flags.clearFlag(ObjectFlag::Indexed); + flagsLater.clearFlag(ObjectFlag::Indexed); + MOZ_RELEASE_ASSERT((flags.toRaw() & flagsLater.toRaw()) == flags.toRaw()); + } + + // If the HadGetterSetterChange flag wasn't set, all GetterSetter slots must + // be unchanged. + if (!later.objectFlags_.hasFlag(ObjectFlag::HadGetterSetterChange)) { + for (size_t i = 0; i < slots_.length(); i++) { + if (slots_[i].isPrivateGCThing() && + slots_[i].toGCThing()->is<GetterSetter>()) { + MOZ_RELEASE_ASSERT(i < later.slots_.length()); + MOZ_RELEASE_ASSERT(later.slots_[i] == slots_[i]); + } + } + } +} + +// static +ShapeSnapshotObject* ShapeSnapshotObject::create(JSContext* cx, + HandleObject obj) { + Rooted<UniquePtr<ShapeSnapshot>> snapshot(cx, + cx->make_unique<ShapeSnapshot>(cx)); + if (!snapshot || !snapshot->init(obj)) { + return nullptr; + } + + auto* snapshotObj = NewObjectWithGivenProto<ShapeSnapshotObject>(cx, nullptr); + if (!snapshotObj) { + return nullptr; + } + snapshotObj->initReservedSlot(SnapshotSlot, PrivateValue(snapshot.release())); + return snapshotObj; +} + +static bool CreateShapeSnapshot(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "createShapeSnapshot requires an object argument"); + return false; + } + + RootedObject obj(cx, &args[0].toObject()); + auto* res = ShapeSnapshotObject::create(cx, obj); + if (!res) { + return false; + } + + res->snapshot().check(cx, res->snapshot()); + + args.rval().setObject(*res); + return true; +} + +static bool CheckShapeSnapshot(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.get(0).isObject() || + !args[0].toObject().is<ShapeSnapshotObject>()) { + JS_ReportErrorASCII(cx, "checkShapeSnapshot requires a snapshot argument"); + return false; + } + + // Get the object to use from the snapshot if the second argument is not an + // object. + RootedObject obj(cx); + if (args.get(1).isObject()) { + obj = &args[1].toObject(); + } else { + auto& snapshot = args[0].toObject().as<ShapeSnapshotObject>().snapshot(); + obj = snapshot.object(); + } + + RootedObject otherSnapshot(cx, ShapeSnapshotObject::create(cx, obj)); + if (!otherSnapshot) { + return false; + } + + auto& snapshot1 = args[0].toObject().as<ShapeSnapshotObject>().snapshot(); + auto& snapshot2 = otherSnapshot->as<ShapeSnapshotObject>().snapshot(); + snapshot1.check(cx, snapshot2); + + args.rval().setUndefined(); + return true; +} + +#if defined(DEBUG) || defined(JS_JITSPEW) +static bool DumpObject(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject obj(cx, ToObject(cx, args.get(0))); + if (!obj) { + return false; + } + + DumpObject(obj); + + args.rval().setUndefined(); + return true; +} +#endif + +static bool SharedMemoryEnabled(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean( + cx->realm()->creationOptions().getSharedMemoryAndAtomicsEnabled()); + return true; +} + +static bool SharedArrayRawBufferRefcount(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() != 1 || !args[0].isObject()) { + JS_ReportErrorASCII(cx, "Expected SharedArrayBuffer object"); + return false; + } + RootedObject obj(cx, &args[0].toObject()); + if (!obj->is<SharedArrayBufferObject>()) { + JS_ReportErrorASCII(cx, "Expected SharedArrayBuffer object"); + return false; + } + args.rval().setInt32( + obj->as<SharedArrayBufferObject>().rawBufferObject()->refcount()); + return true; +} + +#ifdef NIGHTLY_BUILD +static bool ObjectAddress(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (js::SupportDifferentialTesting()) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, + "Function unavailable in differential testing mode."); + return false; + } + + if (args.length() != 1) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + if (!args[0].isObject()) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Expected object"); + return false; + } + + void* ptr = js::UncheckedUnwrap(&args[0].toObject(), true); + char buffer[64]; + SprintfLiteral(buffer, "%p", ptr); + + return ReturnStringCopy(cx, args, buffer); +} + +static bool SharedAddress(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (js::SupportDifferentialTesting()) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, + "Function unavailable in differential testing mode."); + return false; + } + + if (args.length() != 1) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + if (!args[0].isObject()) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Expected object"); + return false; + } + + RootedObject obj(cx, CheckedUnwrapStatic(&args[0].toObject())); + if (!obj) { + ReportAccessDenied(cx); + return false; + } + if (!obj->is<SharedArrayBufferObject>()) { + JS_ReportErrorASCII(cx, "Argument must be a SharedArrayBuffer"); + return false; + } + char buffer[64]; + uint32_t nchar = SprintfLiteral( + buffer, "%p", + obj->as<SharedArrayBufferObject>().dataPointerShared().unwrap( + /*safeish*/)); + + JSString* str = JS_NewStringCopyN(cx, buffer, nchar); + if (!str) { + return false; + } + + args.rval().setString(str); + + return true; +} +#endif + +static bool HasInvalidatedTeleporting(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isObject()) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Expected single object argument"); + return false; + } + + args.rval().setBoolean(args[0].toObject().hasInvalidatedTeleporting()); + return true; +} + +static bool DumpBacktrace(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + DumpBacktrace(cx); + args.rval().setUndefined(); + return true; +} + +static bool GetBacktrace(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + bool showArgs = false; + bool showLocals = false; + bool showThisProps = false; + + if (args.length() > 1) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "Too many arguments"); + return false; + } + + if (args.length() == 1) { + RootedObject cfg(cx, ToObject(cx, args[0])); + if (!cfg) { + return false; + } + RootedValue v(cx); + + if (!JS_GetProperty(cx, cfg, "args", &v)) { + return false; + } + showArgs = ToBoolean(v); + + if (!JS_GetProperty(cx, cfg, "locals", &v)) { + return false; + } + showLocals = ToBoolean(v); + + if (!JS_GetProperty(cx, cfg, "thisprops", &v)) { + return false; + } + showThisProps = ToBoolean(v); + } + + JS::UniqueChars buf = + JS::FormatStackDump(cx, showArgs, showLocals, showThisProps); + if (!buf) { + return false; + } + + size_t len; + UniqueTwoByteChars ucbuf(JS::LossyUTF8CharsToNewTwoByteCharsZ( + cx, JS::UTF8Chars(buf.get(), strlen(buf.get())), + &len, js::MallocArena) + .get()); + if (!ucbuf) { + return false; + } + JSString* str = JS_NewUCStringCopyN(cx, ucbuf.get(), len); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static bool ReportOutOfMemory(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + JS_ReportOutOfMemory(cx); + cx->clearPendingException(); + args.rval().setUndefined(); + return true; +} + +static bool ThrowOutOfMemory(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportOutOfMemory(cx); + return false; +} + +static bool ReportLargeAllocationFailure(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + size_t bytes = JSRuntime::LARGE_ALLOCATION; + if (args.length() >= 1) { + if (!args[0].isInt32()) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, + "First argument must be an integer if specified."); + return false; + } + bytes = args[0].toInt32(); + } + + void* buf = cx->runtime()->onOutOfMemoryCanGC(AllocFunction::Malloc, + js::MallocArena, bytes); + + js_free(buf); + args.rval().setUndefined(); + return true; +} + +namespace heaptools { + +using EdgeName = UniqueTwoByteChars; + +// An edge to a node from its predecessor in a path through the graph. +class BackEdge { + // The node from which this edge starts. + JS::ubi::Node predecessor_; + + // The name of this edge. + EdgeName name_; + + public: + BackEdge() : name_(nullptr) {} + // Construct an initialized back edge, taking ownership of |name|. + BackEdge(JS::ubi::Node predecessor, EdgeName name) + : predecessor_(predecessor), name_(std::move(name)) {} + BackEdge(BackEdge&& rhs) + : predecessor_(rhs.predecessor_), name_(std::move(rhs.name_)) {} + BackEdge& operator=(BackEdge&& rhs) { + MOZ_ASSERT(&rhs != this); + this->~BackEdge(); + new (this) BackEdge(std::move(rhs)); + return *this; + } + + EdgeName forgetName() { return std::move(name_); } + JS::ubi::Node predecessor() const { return predecessor_; } + + private: + // No copy constructor or copying assignment. + BackEdge(const BackEdge&) = delete; + BackEdge& operator=(const BackEdge&) = delete; +}; + +// A path-finding handler class for use with JS::ubi::BreadthFirst. +struct FindPathHandler { + using NodeData = BackEdge; + using Traversal = JS::ubi::BreadthFirst<FindPathHandler>; + + FindPathHandler(JSContext* cx, JS::ubi::Node start, JS::ubi::Node target, + MutableHandle<GCVector<Value>> nodes, Vector<EdgeName>& edges) + : cx(cx), + start(start), + target(target), + foundPath(false), + nodes(nodes), + edges(edges) {} + + bool operator()(Traversal& traversal, JS::ubi::Node origin, + const JS::ubi::Edge& edge, BackEdge* backEdge, bool first) { + // We take care of each node the first time we visit it, so there's + // nothing to be done on subsequent visits. + if (!first) { + return true; + } + + // Record how we reached this node. This is the last edge on a + // shortest path to this node. + EdgeName edgeName = + DuplicateStringToArena(js::StringBufferArena, cx, edge.name.get()); + if (!edgeName) { + return false; + } + *backEdge = BackEdge(origin, std::move(edgeName)); + + // Have we reached our final target node? + if (edge.referent == target) { + // Record the path that got us here, which must be a shortest path. + if (!recordPath(traversal, backEdge)) { + return false; + } + foundPath = true; + traversal.stop(); + } + + return true; + } + + // We've found a path to our target. Walk the backlinks to produce the + // (reversed) path, saving the path in |nodes| and |edges|. |nodes| is + // rooted, so it can hold the path's nodes as we leave the scope of + // the AutoCheckCannotGC. Note that nodes are added to |visited| after we + // return from operator() so we have to pass the target BackEdge* to this + // function. + bool recordPath(Traversal& traversal, BackEdge* targetBackEdge) { + JS::ubi::Node here = target; + + do { + BackEdge* backEdge = targetBackEdge; + if (here != target) { + Traversal::NodeMap::Ptr p = traversal.visited.lookup(here); + MOZ_ASSERT(p); + backEdge = &p->value(); + } + JS::ubi::Node predecessor = backEdge->predecessor(); + if (!nodes.append(predecessor.exposeToJS()) || + !edges.append(backEdge->forgetName())) { + return false; + } + here = predecessor; + } while (here != start); + + return true; + } + + JSContext* cx; + + // The node we're starting from. + JS::ubi::Node start; + + // The node we're looking for. + JS::ubi::Node target; + + // True if we found a path to target, false if we didn't. + bool foundPath; + + // The nodes and edges of the path --- should we find one. The path is + // stored in reverse order, because that's how it's easiest for us to + // construct it: + // - edges[i] is the name of the edge from nodes[i] to nodes[i-1]. + // - edges[0] is the name of the edge from nodes[0] to the target. + // - The last node, nodes[n-1], is the start node. + MutableHandle<GCVector<Value>> nodes; + Vector<EdgeName>& edges; +}; + +} // namespace heaptools + +static bool FindPath(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "findPath", 2)) { + return false; + } + + // We don't ToString non-objects given as 'start' or 'target', because this + // test is all about object identity, and ToString doesn't preserve that. + // Non-GCThing endpoints don't make much sense. + if (!args[0].isObject() && !args[0].isString() && !args[0].isSymbol()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, args[0], + nullptr, "not an object, string, or symbol"); + return false; + } + + if (!args[1].isObject() && !args[1].isString() && !args[1].isSymbol()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, args[0], + nullptr, "not an object, string, or symbol"); + return false; + } + + Rooted<GCVector<Value>> nodes(cx, GCVector<Value>(cx)); + Vector<heaptools::EdgeName> edges(cx); + + { + // We can't tolerate the GC moving things around while we're searching + // the heap. Check that nothing we do causes a GC. + JS::AutoCheckCannotGC autoCannotGC; + + JS::ubi::Node start(args[0]), target(args[1]); + + heaptools::FindPathHandler handler(cx, start, target, &nodes, edges); + heaptools::FindPathHandler::Traversal traversal(cx, handler, autoCannotGC); + if (!traversal.addStart(start)) { + ReportOutOfMemory(cx); + return false; + } + + if (!traversal.traverse()) { + if (!cx->isExceptionPending()) { + ReportOutOfMemory(cx); + } + return false; + } + + if (!handler.foundPath) { + // We didn't find any paths from the start to the target. + args.rval().setUndefined(); + return true; + } + } + + // |nodes| and |edges| contain the path from |start| to |target|, reversed. + // Construct a JavaScript array describing the path from the start to the + // target. Each element has the form: + // + // { + // node: <object or string or symbol>, + // edge: <string describing outgoing edge from node> + // } + // + // or, if the node is some internal thing that isn't a proper JavaScript + // value: + // + // { node: undefined, edge: <string> } + size_t length = nodes.length(); + Rooted<ArrayObject*> result(cx, NewDenseFullyAllocatedArray(cx, length)); + if (!result) { + return false; + } + result->ensureDenseInitializedLength(0, length); + + // Walk |nodes| and |edges| in the stored order, and construct the result + // array in start-to-target order. + for (size_t i = 0; i < length; i++) { + // Build an object describing the node and edge. + RootedObject obj(cx, NewPlainObject(cx)); + if (!obj) { + return false; + } + + // Only define the "node" property if we're not fuzzing, to prevent the + // fuzzers from messing with internal objects that we don't want to expose + // to arbitrary JS. + if (!fuzzingSafe) { + RootedValue wrapped(cx, nodes[i]); + if (!cx->compartment()->wrap(cx, &wrapped)) { + return false; + } + + if (!JS_DefineProperty(cx, obj, "node", wrapped, JSPROP_ENUMERATE)) { + return false; + } + } + + heaptools::EdgeName edgeName = std::move(edges[i]); + + size_t edgeNameLength = js_strlen(edgeName.get()); + RootedString edgeStr( + cx, NewString<CanGC>(cx, std::move(edgeName), edgeNameLength)); + if (!edgeStr) { + return false; + } + + if (!JS_DefineProperty(cx, obj, "edge", edgeStr, JSPROP_ENUMERATE)) { + return false; + } + + result->setDenseElement(length - i - 1, ObjectValue(*obj)); + } + + args.rval().setObject(*result); + return true; +} + +static bool ShortestPaths(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "shortestPaths", 1)) { + return false; + } + + if (!args[0].isObject() || !args[0].toObject().is<ArrayObject>()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, args[0], + nullptr, "not an array object"); + return false; + } + + Rooted<ArrayObject*> objs(cx, &args[0].toObject().as<ArrayObject>()); + + RootedValue start(cx, NullValue()); + int32_t maxNumPaths = 3; + + if (!args.get(1).isUndefined()) { + if (!args[1].isObject()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, args[1], + nullptr, "not an options object"); + return false; + } + + RootedObject options(cx, &args[1].toObject()); + bool exists; + if (!JS_HasProperty(cx, options, "start", &exists)) { + return false; + } + if (exists) { + if (!JS_GetProperty(cx, options, "start", &start)) { + return false; + } + + // Non-GCThing endpoints don't make much sense. + if (!start.isGCThing()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, start, + nullptr, "not a GC thing"); + return false; + } + } + + RootedValue v(cx, Int32Value(maxNumPaths)); + if (!JS_HasProperty(cx, options, "maxNumPaths", &exists)) { + return false; + } + if (exists) { + if (!JS_GetProperty(cx, options, "maxNumPaths", &v)) { + return false; + } + if (!JS::ToInt32(cx, v, &maxNumPaths)) { + return false; + } + } + if (maxNumPaths <= 0) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, v, + nullptr, "not greater than 0"); + return false; + } + } + + // Ensure we have at least one target. + size_t length = objs->getDenseInitializedLength(); + if (length == 0) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, args[0], + nullptr, + "not a dense array object with one or more elements"); + return false; + } + + for (size_t i = 0; i < length; i++) { + RootedValue el(cx, objs->getDenseElement(i)); + if (!el.isGCThing()) { + JS_ReportErrorASCII(cx, "Each target must be a GC thing"); + return false; + } + } + + // We accumulate the results into a GC-stable form, due to the fact that the + // JS::ubi::ShortestPaths lifetime (when operating on the live heap graph) + // is bounded within an AutoCheckCannotGC. + Rooted<GCVector<GCVector<GCVector<Value>>>> values( + cx, GCVector<GCVector<GCVector<Value>>>(cx)); + Vector<Vector<Vector<JS::ubi::EdgeName>>> names(cx); + + { + JS::ubi::Node root; + + JS::ubi::RootList rootList(cx, true); + if (start.isNull()) { + auto [ok, nogc] = rootList.init(); + (void)nogc; // Old compilers get anxious about nogc being unused. + if (!ok) { + ReportOutOfMemory(cx); + return false; + } + root = JS::ubi::Node(&rootList); + } else { + root = JS::ubi::Node(start); + } + JS::AutoCheckCannotGC noGC(cx); + + JS::ubi::NodeSet targets; + + for (size_t i = 0; i < length; i++) { + RootedValue val(cx, objs->getDenseElement(i)); + JS::ubi::Node node(val); + if (!targets.put(node)) { + ReportOutOfMemory(cx); + return false; + } + } + + auto maybeShortestPaths = JS::ubi::ShortestPaths::Create( + cx, noGC, maxNumPaths, root, std::move(targets)); + if (maybeShortestPaths.isNothing()) { + ReportOutOfMemory(cx); + return false; + } + auto& shortestPaths = *maybeShortestPaths; + + for (size_t i = 0; i < length; i++) { + if (!values.append(GCVector<GCVector<Value>>(cx)) || + !names.append(Vector<Vector<JS::ubi::EdgeName>>(cx))) { + return false; + } + + RootedValue val(cx, objs->getDenseElement(i)); + JS::ubi::Node target(val); + + bool ok = shortestPaths.forEachPath(target, [&](JS::ubi::Path& path) { + Rooted<GCVector<Value>> pathVals(cx, GCVector<Value>(cx)); + Vector<JS::ubi::EdgeName> pathNames(cx); + + for (auto& part : path) { + if (!pathVals.append(part->predecessor().exposeToJS()) || + !pathNames.append(std::move(part->name()))) { + return false; + } + } + + return values.back().append(std::move(pathVals.get())) && + names.back().append(std::move(pathNames)); + }); + + if (!ok) { + return false; + } + } + } + + MOZ_ASSERT(values.length() == names.length()); + MOZ_ASSERT(values.length() == length); + + Rooted<ArrayObject*> results(cx, NewDenseFullyAllocatedArray(cx, length)); + if (!results) { + return false; + } + results->ensureDenseInitializedLength(0, length); + + for (size_t i = 0; i < length; i++) { + size_t numPaths = values[i].length(); + MOZ_ASSERT(names[i].length() == numPaths); + + Rooted<ArrayObject*> pathsArray(cx, + NewDenseFullyAllocatedArray(cx, numPaths)); + if (!pathsArray) { + return false; + } + pathsArray->ensureDenseInitializedLength(0, numPaths); + + for (size_t j = 0; j < numPaths; j++) { + size_t pathLength = values[i][j].length(); + MOZ_ASSERT(names[i][j].length() == pathLength); + + Rooted<ArrayObject*> path(cx, + NewDenseFullyAllocatedArray(cx, pathLength)); + if (!path) { + return false; + } + path->ensureDenseInitializedLength(0, pathLength); + + for (size_t k = 0; k < pathLength; k++) { + Rooted<PlainObject*> part(cx, NewPlainObject(cx)); + if (!part) { + return false; + } + + RootedValue predecessor(cx, values[i][j][k]); + if (!cx->compartment()->wrap(cx, &predecessor) || + !JS_DefineProperty(cx, part, "predecessor", predecessor, + JSPROP_ENUMERATE)) { + return false; + } + + if (names[i][j][k]) { + RootedString edge(cx, + NewStringCopyZ<CanGC>(cx, names[i][j][k].get())); + if (!edge || + !JS_DefineProperty(cx, part, "edge", edge, JSPROP_ENUMERATE)) { + return false; + } + } + + path->setDenseElement(k, ObjectValue(*part)); + } + + pathsArray->setDenseElement(j, ObjectValue(*path)); + } + + results->setDenseElement(i, ObjectValue(*pathsArray)); + } + + args.rval().setObject(*results); + return true; +} + +static bool EvalReturningScope(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "evalReturningScope", 1)) { + return false; + } + + RootedString str(cx, ToString(cx, args[0])); + if (!str) { + return false; + } + + RootedObject global(cx); + if (args.hasDefined(1)) { + global = ToObject(cx, args[1]); + if (!global) { + return false; + } + } + + JS::AutoFilename filename; + unsigned lineno; + + JS::DescribeScriptedCaller(cx, &filename, &lineno); + + JS::CompileOptions options(cx); + options.setFileAndLine(filename.get(), lineno); + options.setNoScriptRval(true); + options.setNonSyntacticScope(true); + + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, str)) { + return false; + } + JS::SourceText<char16_t> srcBuf; + if (!srcBuf.initMaybeBorrowed(cx, linearChars)) { + return false; + } + + if (global) { + global = CheckedUnwrapDynamic(global, cx, /* stopAtWindowProxy = */ false); + if (!global) { + JS_ReportErrorASCII(cx, "Permission denied to access global"); + return false; + } + if (!global->is<GlobalObject>()) { + JS_ReportErrorASCII(cx, "Argument must be a global object"); + return false; + } + } else { + global = JS::CurrentGlobalOrNull(cx); + } + + RootedObject varObj(cx); + + { + // ExecuteInFrameScriptEnvironment requires the script be in the same + // realm as the global. The script compilation should be done after + // switching globals. + AutoRealm ar(cx, global); + + RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + if (!script) { + return false; + } + + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + if (!obj) { + return false; + } + + RootedObject lexicalScope(cx); + if (!js::ExecuteInFrameScriptEnvironment(cx, obj, script, &lexicalScope)) { + return false; + } + + varObj = lexicalScope->enclosingEnvironment()->enclosingEnvironment(); + MOZ_ASSERT(varObj->is<NonSyntacticVariablesObject>()); + } + + RootedValue varObjVal(cx, ObjectValue(*varObj)); + if (!cx->compartment()->wrap(cx, &varObjVal)) { + return false; + } + + args.rval().set(varObjVal); + return true; +} + +static bool ByteSize(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + mozilla::MallocSizeOf mallocSizeOf = cx->runtime()->debuggerMallocSizeOf; + + { + // We can't tolerate the GC moving things around while we're using a + // ubi::Node. Check that nothing we do causes a GC. + JS::AutoCheckCannotGC autoCannotGC; + + JS::ubi::Node node = args.get(0); + if (node) { + args.rval().setNumber(uint32_t(node.size(mallocSizeOf))); + } else { + args.rval().setUndefined(); + } + } + return true; +} + +static bool ByteSizeOfScript(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "byteSizeOfScript", 1)) { + return false; + } + if (!args[0].isObject() || !args[0].toObject().is<JSFunction>()) { + JS_ReportErrorASCII(cx, "Argument must be a Function object"); + return false; + } + + RootedFunction fun(cx, &args[0].toObject().as<JSFunction>()); + if (fun->isNativeFun()) { + JS_ReportErrorASCII(cx, "Argument must be a scripted function"); + return false; + } + + RootedScript script(cx, JSFunction::getOrCreateScript(cx, fun)); + if (!script) { + return false; + } + + mozilla::MallocSizeOf mallocSizeOf = cx->runtime()->debuggerMallocSizeOf; + + { + // We can't tolerate the GC moving things around while we're using a + // ubi::Node. Check that nothing we do causes a GC. + JS::AutoCheckCannotGC autoCannotGC; + + JS::ubi::Node node = script; + if (node) { + args.rval().setNumber(uint32_t(node.size(mallocSizeOf))); + } else { + args.rval().setUndefined(); + } + } + return true; +} + +static bool SetImmutablePrototype(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.get(0).isObject()) { + JS_ReportErrorASCII(cx, "setImmutablePrototype: object expected"); + return false; + } + + RootedObject obj(cx, &args[0].toObject()); + + bool succeeded; + if (!js::SetImmutablePrototype(cx, obj, &succeeded)) { + return false; + } + + args.rval().setBoolean(succeeded); + return true; +} + +#ifdef DEBUG +static bool DumpStringRepresentation(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString str(cx, ToString(cx, args.get(0))); + if (!str) { + return false; + } + + Fprinter out(stderr); + str->dumpRepresentation(out, 0); + + args.rval().setUndefined(); + return true; +} + +static bool GetStringRepresentation(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedString str(cx, ToString(cx, args.get(0))); + if (!str) { + return false; + } + + Sprinter out(cx, true); + if (!out.init()) { + return false; + } + str->dumpRepresentation(out, 0); + + if (out.hadOutOfMemory()) { + return false; + } + + JSString* rep = JS_NewStringCopyN(cx, out.string(), out.getOffset()); + if (!rep) { + return false; + } + + args.rval().setString(rep); + return true; +} + +#endif + +static bool ParseCompileOptionsForModule(JSContext* cx, + JS::CompileOptions& options, + JS::Handle<JSObject*> opts, + bool& isModule) { + JS::Rooted<JS::Value> v(cx); + + if (!JS_GetProperty(cx, opts, "module", &v)) { + return false; + } + if (!v.isUndefined() && JS::ToBoolean(v)) { + options.setModule(); + isModule = true; + } else { + isModule = false; + } + + return true; +} + +static bool ParseCompileOptionsForInstantiate(JSContext* cx, + JS::CompileOptions& options, + JS::Handle<JSObject*> opts, + bool& prepareForInstantiate) { + JS::Rooted<JS::Value> v(cx); + + if (!JS_GetProperty(cx, opts, "prepareForInstantiate", &v)) { + return false; + } + if (!v.isUndefined()) { + prepareForInstantiate = JS::ToBoolean(v); + } else { + prepareForInstantiate = false; + } + + return true; +} + +static bool CompileToStencil(JSContext* cx, uint32_t argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "compileToStencil", 1)) { + return false; + } + if (!args[0].isString()) { + const char* typeName = InformalValueTypeName(args[0]); + JS_ReportErrorASCII(cx, "expected string to parse, got %s", typeName); + return false; + } + + RootedString src(cx, ToString<CanGC>(cx, args[0])); + if (!src) { + return false; + } + + /* Linearize the string to obtain a char16_t* range. */ + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, src)) { + return false; + } + JS::SourceText<char16_t> srcBuf; + if (!srcBuf.initMaybeBorrowed(cx, linearChars)) { + return false; + } + + CompileOptions options(cx); + RootedString displayURL(cx); + RootedString sourceMapURL(cx); + UniqueChars fileNameBytes; + bool isModule = false; + bool prepareForInstantiate = false; + if (args.length() == 2) { + if (!args[1].isObject()) { + JS_ReportErrorASCII( + cx, "compileToStencil: The 2nd argument must be an object"); + return false; + } + + RootedObject opts(cx, &args[1].toObject()); + + if (!js::ParseCompileOptions(cx, options, opts, &fileNameBytes)) { + return false; + } + if (!ParseCompileOptionsForModule(cx, options, opts, isModule)) { + return false; + } + if (!ParseCompileOptionsForInstantiate(cx, options, opts, + prepareForInstantiate)) { + return false; + } + if (!js::ParseSourceOptions(cx, opts, &displayURL, &sourceMapURL)) { + return false; + } + } + + AutoReportFrontendContext fc(cx); + RefPtr<JS::Stencil> stencil; + JS::CompilationStorage compileStorage; + if (isModule) { + stencil = + JS::CompileModuleScriptToStencil(&fc, options, srcBuf, compileStorage); + } else { + stencil = + JS::CompileGlobalScriptToStencil(&fc, options, srcBuf, compileStorage); + } + if (!stencil) { + return false; + } + + if (!SetSourceOptions(cx, &fc, stencil->source, displayURL, sourceMapURL)) { + return false; + } + + JS::InstantiationStorage storage; + if (prepareForInstantiate) { + if (!JS::PrepareForInstantiate(&fc, compileStorage, *stencil, storage)) { + return false; + } + } + + Rooted<js::StencilObject*> stencilObj( + cx, js::StencilObject::create(cx, std::move(stencil))); + if (!stencilObj) { + return false; + } + + args.rval().setObject(*stencilObj); + return true; +} + +static bool EvalStencil(JSContext* cx, uint32_t argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "evalStencil", 1)) { + return false; + } + + /* Prepare the input byte array. */ + if (!args[0].isObject() || !args[0].toObject().is<js::StencilObject>()) { + JS_ReportErrorASCII(cx, "evalStencil: Stencil object expected"); + return false; + } + Rooted<js::StencilObject*> stencilObj( + cx, &args[0].toObject().as<js::StencilObject>()); + + if (stencilObj->stencil()->isModule()) { + JS_ReportErrorASCII(cx, + "evalStencil: Module stencil cannot be evaluated. Use " + "instantiateModuleStencil instead"); + return false; + } + + CompileOptions options(cx); + UniqueChars fileNameBytes; + Rooted<JS::Value> privateValue(cx); + Rooted<JSString*> elementAttributeName(cx); + if (args.length() == 2) { + if (!args[1].isObject()) { + JS_ReportErrorASCII(cx, + "evalStencil: The 2nd argument must be an object"); + return false; + } + + RootedObject opts(cx, &args[1].toObject()); + + if (!js::ParseCompileOptions(cx, options, opts, &fileNameBytes)) { + return false; + } + if (!js::ParseDebugMetadata(cx, opts, &privateValue, + &elementAttributeName)) { + return false; + } + } + + bool useDebugMetadata = !privateValue.isUndefined() || elementAttributeName; + + JS::InstantiateOptions instantiateOptions(options); + if (useDebugMetadata) { + instantiateOptions.hideScriptFromDebugger = true; + } + RootedScript script(cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, + stencilObj->stencil())); + if (!script) { + return false; + } + + if (useDebugMetadata) { + instantiateOptions.hideScriptFromDebugger = false; + if (!JS::UpdateDebugMetadata(cx, script, instantiateOptions, privateValue, + elementAttributeName, nullptr, nullptr)) { + return false; + } + } + + RootedValue retVal(cx); + if (!JS_ExecuteScript(cx, script, &retVal)) { + return false; + } + + args.rval().set(retVal); + return true; +} + +static bool CompileToStencilXDR(JSContext* cx, uint32_t argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "compileToStencilXDR", 1)) { + return false; + } + + RootedString src(cx, ToString<CanGC>(cx, args[0])); + if (!src) { + return false; + } + + /* Linearize the string to obtain a char16_t* range. */ + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, src)) { + return false; + } + JS::SourceText<char16_t> srcBuf; + if (!srcBuf.initMaybeBorrowed(cx, linearChars)) { + return false; + } + + CompileOptions options(cx); + RootedString displayURL(cx); + RootedString sourceMapURL(cx); + UniqueChars fileNameBytes; + bool isModule = false; + if (args.length() == 2) { + if (!args[1].isObject()) { + JS_ReportErrorASCII( + cx, "compileToStencilXDR: The 2nd argument must be an object"); + return false; + } + + RootedObject opts(cx, &args[1].toObject()); + + if (!js::ParseCompileOptions(cx, options, opts, &fileNameBytes)) { + return false; + } + if (!ParseCompileOptionsForModule(cx, options, opts, isModule)) { + return false; + } + if (!js::ParseSourceOptions(cx, opts, &displayURL, &sourceMapURL)) { + return false; + } + } + + /* Compile the script text to stencil. */ + AutoReportFrontendContext fc(cx); + frontend::NoScopeBindingCache scopeCache; + Rooted<frontend::CompilationInput> input(cx, + frontend::CompilationInput(options)); + UniquePtr<frontend::ExtensibleCompilationStencil> stencil; + if (isModule) { + stencil = frontend::ParseModuleToExtensibleStencil( + cx, &fc, cx->tempLifoAlloc(), input.get(), &scopeCache, srcBuf); + } else { + stencil = frontend::CompileGlobalScriptToExtensibleStencil( + cx, &fc, input.get(), &scopeCache, srcBuf, ScopeKind::Global); + } + if (!stencil) { + return false; + } + + if (!SetSourceOptions(cx, &fc, stencil->source, displayURL, sourceMapURL)) { + return false; + } + + /* Serialize the stencil to XDR. */ + JS::TranscodeBuffer xdrBytes; + { + frontend::BorrowingCompilationStencil borrowingStencil(*stencil); + bool succeeded = false; + if (!borrowingStencil.serializeStencils(cx, input.get(), xdrBytes, + &succeeded)) { + return false; + } + if (!succeeded) { + fc.clearAutoReport(); + JS_ReportErrorASCII(cx, "Encoding failure"); + return false; + } + } + + Rooted<StencilXDRBufferObject*> xdrObj( + cx, + StencilXDRBufferObject::create(cx, xdrBytes.begin(), xdrBytes.length())); + if (!xdrObj) { + return false; + } + + args.rval().setObject(*xdrObj); + return true; +} + +static bool EvalStencilXDR(JSContext* cx, uint32_t argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "evalStencilXDR", 1)) { + return false; + } + + /* Prepare the input byte array. */ + if (!args[0].isObject() || !args[0].toObject().is<StencilXDRBufferObject>()) { + JS_ReportErrorASCII(cx, "evalStencilXDR: stencil XDR object expected"); + return false; + } + Rooted<StencilXDRBufferObject*> xdrObj( + cx, &args[0].toObject().as<StencilXDRBufferObject>()); + MOZ_ASSERT(xdrObj->hasBuffer()); + + CompileOptions options(cx); + UniqueChars fileNameBytes; + Rooted<JS::Value> privateValue(cx); + Rooted<JSString*> elementAttributeName(cx); + if (args.length() == 2) { + if (!args[1].isObject()) { + JS_ReportErrorASCII(cx, + "evalStencilXDR: The 2nd argument must be an object"); + return false; + } + + RootedObject opts(cx, &args[1].toObject()); + + if (!js::ParseCompileOptions(cx, options, opts, &fileNameBytes)) { + return false; + } + if (!js::ParseDebugMetadata(cx, opts, &privateValue, + &elementAttributeName)) { + return false; + } + } + + /* Prepare the CompilationStencil for decoding. */ + AutoReportFrontendContext fc(cx); + frontend::CompilationStencil stencil(nullptr); + + /* Deserialize the stencil from XDR. */ + JS::TranscodeRange xdrRange(xdrObj->buffer(), xdrObj->bufferLength()); + bool succeeded = false; + if (!stencil.deserializeStencils(&fc, options, xdrRange, &succeeded)) { + return false; + } + if (!succeeded) { + fc.clearAutoReport(); + JS_ReportErrorASCII(cx, "Decoding failure"); + return false; + } + + if (stencil.isModule()) { + fc.clearAutoReport(); + JS_ReportErrorASCII(cx, + "evalStencilXDR: Module stencil cannot be evaluated. " + "Use instantiateModuleStencilXDR instead"); + return false; + } + + bool useDebugMetadata = !privateValue.isUndefined() || elementAttributeName; + + JS::InstantiateOptions instantiateOptions(options); + if (useDebugMetadata) { + instantiateOptions.hideScriptFromDebugger = true; + } + RootedScript script( + cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, &stencil)); + if (!script) { + return false; + } + + if (useDebugMetadata) { + instantiateOptions.hideScriptFromDebugger = false; + if (!JS::UpdateDebugMetadata(cx, script, instantiateOptions, privateValue, + elementAttributeName, nullptr, nullptr)) { + return false; + } + } + + RootedValue retVal(cx); + if (!JS_ExecuteScript(cx, script, &retVal)) { + return false; + } + + args.rval().set(retVal); + return true; +} + +static bool GetExceptionInfo(JSContext* cx, uint32_t argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!args.requireAtLeast(cx, "getExceptionInfo", 1)) { + return false; + } + + if (!args[0].isObject() || !args[0].toObject().is<JSFunction>()) { + JS_ReportErrorASCII(cx, "getExceptionInfo: expected function argument"); + return false; + } + + RootedValue rval(cx); + if (JS_CallFunctionValue(cx, nullptr, args[0], JS::HandleValueArray::empty(), + &rval)) { + // Function didn't throw. + args.rval().setNull(); + return true; + } + + // We currently don't support interrupts or forced returns. + if (!cx->isExceptionPending()) { + JS_ReportErrorASCII(cx, "getExceptionInfo: unsupported exception status"); + return false; + } + + RootedValue excVal(cx); + Rooted<SavedFrame*> stack(cx); + if (!GetAndClearExceptionAndStack(cx, &excVal, &stack)) { + return false; + } + + RootedValue stackVal(cx); + if (stack) { + RootedString stackString(cx); + if (!BuildStackString(cx, cx->realm()->principals(), stack, &stackString)) { + return false; + } + stackVal.setString(stackString); + } else { + stackVal.setNull(); + } + + RootedObject obj(cx, NewPlainObject(cx)); + if (!obj) { + return false; + } + + if (!JS_DefineProperty(cx, obj, "exception", excVal, JSPROP_ENUMERATE)) { + return false; + } + + if (!JS_DefineProperty(cx, obj, "stack", stackVal, JSPROP_ENUMERATE)) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +class AllocationMarkerObject : public NativeObject { + public: + static const JSClass class_; +}; + +const JSClass AllocationMarkerObject::class_ = {"AllocationMarker"}; + +static bool AllocationMarker(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + bool allocateInsideNursery = true; + if (args.length() > 0 && args[0].isObject()) { + RootedObject options(cx, &args[0].toObject()); + + RootedValue nurseryVal(cx); + if (!JS_GetProperty(cx, options, "nursery", &nurseryVal)) { + return false; + } + allocateInsideNursery = ToBoolean(nurseryVal); + } + + JSObject* obj = + allocateInsideNursery + ? NewObjectWithGivenProto<AllocationMarkerObject>(cx, nullptr) + : NewTenuredObjectWithGivenProto<AllocationMarkerObject>(cx, nullptr); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +namespace gcCallback { + +struct MajorGC { + int32_t depth; + int32_t phases; +}; + +static void majorGC(JSContext* cx, JSGCStatus status, JS::GCReason reason, + void* data) { + auto info = static_cast<MajorGC*>(data); + if (!(info->phases & (1 << status))) { + return; + } + + if (info->depth > 0) { + info->depth--; + JS::PrepareForFullGC(cx); + JS::NonIncrementalGC(cx, JS::GCOptions::Normal, JS::GCReason::API); + info->depth++; + } +} + +struct MinorGC { + int32_t phases; + bool active; +}; + +static void minorGC(JSContext* cx, JSGCStatus status, JS::GCReason reason, + void* data) { + auto info = static_cast<MinorGC*>(data); + if (!(info->phases & (1 << status))) { + return; + } + + if (info->active) { + info->active = false; + if (cx->zone() && !cx->zone()->isAtomsZone()) { + cx->runtime()->gc.evictNursery(JS::GCReason::DEBUG_GC); + } + info->active = true; + } +} + +// Process global, should really be runtime-local. +static MajorGC majorGCInfo; +static MinorGC minorGCInfo; + +static void enterNullRealm(JSContext* cx, JSGCStatus status, + JS::GCReason reason, void* data) { + JSAutoNullableRealm enterRealm(cx, nullptr); +} + +} /* namespace gcCallback */ + +static bool SetGCCallback(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + RootedObject opts(cx, ToObject(cx, args[0])); + if (!opts) { + return false; + } + + RootedValue v(cx); + if (!JS_GetProperty(cx, opts, "action", &v)) { + return false; + } + + JSString* str = JS::ToString(cx, v); + if (!str) { + return false; + } + Rooted<JSLinearString*> action(cx, str->ensureLinear(cx)); + if (!action) { + return false; + } + + int32_t phases = 0; + if (StringEqualsLiteral(action, "minorGC") || + StringEqualsLiteral(action, "majorGC")) { + if (!JS_GetProperty(cx, opts, "phases", &v)) { + return false; + } + if (v.isUndefined()) { + phases = (1 << JSGC_END); + } else { + JSString* str = JS::ToString(cx, v); + if (!str) { + return false; + } + JSLinearString* phasesStr = str->ensureLinear(cx); + if (!phasesStr) { + return false; + } + + if (StringEqualsLiteral(phasesStr, "begin")) { + phases = (1 << JSGC_BEGIN); + } else if (StringEqualsLiteral(phasesStr, "end")) { + phases = (1 << JSGC_END); + } else if (StringEqualsLiteral(phasesStr, "both")) { + phases = (1 << JSGC_BEGIN) | (1 << JSGC_END); + } else { + JS_ReportErrorASCII(cx, "Invalid callback phase"); + return false; + } + } + } + + if (StringEqualsLiteral(action, "minorGC")) { + gcCallback::minorGCInfo.phases = phases; + gcCallback::minorGCInfo.active = true; + JS_SetGCCallback(cx, gcCallback::minorGC, &gcCallback::minorGCInfo); + } else if (StringEqualsLiteral(action, "majorGC")) { + if (!JS_GetProperty(cx, opts, "depth", &v)) { + return false; + } + int32_t depth = 1; + if (!v.isUndefined()) { + if (!ToInt32(cx, v, &depth)) { + return false; + } + } + if (depth < 0) { + JS_ReportErrorASCII(cx, "Nesting depth cannot be negative"); + return false; + } + if (depth + gcstats::MAX_PHASE_NESTING > + gcstats::Statistics::MAX_SUSPENDED_PHASES) { + JS_ReportErrorASCII(cx, "Nesting depth too large, would overflow"); + return false; + } + + gcCallback::majorGCInfo.phases = phases; + gcCallback::majorGCInfo.depth = depth; + JS_SetGCCallback(cx, gcCallback::majorGC, &gcCallback::majorGCInfo); + } else if (StringEqualsLiteral(action, "enterNullRealm")) { + JS_SetGCCallback(cx, gcCallback::enterNullRealm, nullptr); + } else { + JS_ReportErrorASCII(cx, "Unknown GC callback action"); + return false; + } + + args.rval().setUndefined(); + return true; +} + +#ifdef DEBUG +static bool EnqueueMark(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + gc::GCRuntime* gc = &cx->runtime()->gc; + + if (args.get(0).isString()) { + RootedString val(cx, args[0].toString()); + if (!val->ensureLinear(cx)) { + return false; + } + if (!gc->appendTestMarkQueue(StringValue(val))) { + JS_ReportOutOfMemory(cx); + return false; + } + } else if (args.get(0).isObject()) { + if (!gc->appendTestMarkQueue(args[0])) { + JS_ReportOutOfMemory(cx); + return false; + } + } else { + JS_ReportErrorASCII(cx, "Argument must be a string or object"); + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool GetMarkQueue(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + const auto& queue = cx->runtime()->gc.getTestMarkQueue(); + + RootedObject result(cx, JS::NewArrayObject(cx, queue.length())); + if (!result) { + return false; + } + for (size_t i = 0; i < queue.length(); i++) { + RootedValue val(cx, queue[i]); + if (!JS_WrapValue(cx, &val)) { + return false; + } + if (!JS_SetElement(cx, result, i, val)) { + return false; + } + } + + args.rval().setObject(*result); + return true; +} + +static bool ClearMarkQueue(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + cx->runtime()->gc.clearTestMarkQueue(); + args.rval().setUndefined(); + return true; +} +#endif // DEBUG + +static bool NurseryStringsEnabled(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setBoolean(cx->zone()->allocNurseryStrings()); + return true; +} + +static bool IsNurseryAllocated(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.get(0).isGCThing()) { + JS_ReportErrorASCII( + cx, "The function takes one argument, which must be a GC thing"); + return false; + } + + args.rval().setBoolean(IsInsideNursery(args[0].toGCThing())); + return true; +} + +static bool GetLcovInfo(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() > 1) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + if (!coverage::IsLCovEnabled()) { + JS_ReportErrorASCII(cx, "Coverage not enabled for process."); + return false; + } + + RootedObject global(cx); + if (args.hasDefined(0)) { + global = ToObject(cx, args[0]); + if (!global) { + JS_ReportErrorASCII(cx, "Permission denied to access global"); + return false; + } + global = CheckedUnwrapDynamic(global, cx, /* stopAtWindowProxy = */ false); + if (!global) { + ReportAccessDenied(cx); + return false; + } + if (!global->is<GlobalObject>()) { + JS_ReportErrorASCII(cx, "Argument must be a global object"); + return false; + } + } else { + global = JS::CurrentGlobalOrNull(cx); + } + + size_t length = 0; + UniqueChars content; + { + AutoRealm ar(cx, global); + content = js::GetCodeCoverageSummary(cx, &length); + } + + if (!content) { + return false; + } + + JSString* str = + JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(content.get(), length)); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +#ifdef DEBUG +static bool SetRNGState(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "SetRNGState", 2)) { + return false; + } + + double d0; + if (!ToNumber(cx, args[0], &d0)) { + return false; + } + + double d1; + if (!ToNumber(cx, args[1], &d1)) { + return false; + } + + uint64_t seed0 = static_cast<uint64_t>(d0); + uint64_t seed1 = static_cast<uint64_t>(d1); + + if (seed0 == 0 && seed1 == 0) { + JS_ReportErrorASCII(cx, "RNG requires non-zero seed"); + return false; + } + + cx->realm()->getOrCreateRandomNumberGenerator().setState(seed0, seed1); + + args.rval().setUndefined(); + return true; +} +#endif + +static bool GetTimeZone(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 0) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + +#ifndef __wasi__ + auto getTimeZone = [](std::time_t* now) -> const char* { + std::tm local{}; +# if defined(_WIN32) + _tzset(); + if (localtime_s(&local, now) == 0) { + return _tzname[local.tm_isdst > 0]; + } +# else + tzset(); +# if defined(HAVE_LOCALTIME_R) + if (localtime_r(now, &local)) { +# else + std::tm* localtm = std::localtime(now); + if (localtm) { + *local = *localtm; +# endif /* HAVE_LOCALTIME_R */ + +# if defined(HAVE_TM_ZONE_TM_GMTOFF) + return local.tm_zone; +# else + return tzname[local.tm_isdst > 0]; +# endif /* HAVE_TM_ZONE_TM_GMTOFF */ + } +# endif /* _WIN32 */ + return nullptr; + }; + + std::time_t now = std::time(nullptr); + if (now != static_cast<std::time_t>(-1)) { + if (const char* tz = getTimeZone(&now)) { + return ReturnStringCopy(cx, args, tz); + } + } +#endif /* __wasi__ */ + args.rval().setUndefined(); + return true; +} + +static bool SetTimeZone(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 1) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + if (!args[0].isString() && !args[0].isUndefined()) { + ReportUsageErrorASCII(cx, callee, + "First argument should be a string or undefined"); + return false; + } + +#ifndef __wasi__ + auto setTimeZone = [](const char* value) { +# if defined(_WIN32) + return _putenv_s("TZ", value) == 0; +# else + return setenv("TZ", value, true) == 0; +# endif /* _WIN32 */ + }; + + auto unsetTimeZone = []() { +# if defined(_WIN32) + return _putenv_s("TZ", "") == 0; +# else + return unsetenv("TZ") == 0; +# endif /* _WIN32 */ + }; + + if (args[0].isString() && !args[0].toString()->empty()) { + Rooted<JSLinearString*> str(cx, args[0].toString()->ensureLinear(cx)); + if (!str) { + return false; + } + + if (!StringIsAscii(str)) { + ReportUsageErrorASCII(cx, callee, + "First argument contains non-ASCII characters"); + return false; + } + + UniqueChars timeZone = JS_EncodeStringToASCII(cx, str); + if (!timeZone) { + return false; + } + + if (!setTimeZone(timeZone.get())) { + JS_ReportErrorASCII(cx, "Failed to set 'TZ' environment variable"); + return false; + } + } else { + if (!unsetTimeZone()) { + JS_ReportErrorASCII(cx, "Failed to unset 'TZ' environment variable"); + return false; + } + } + +# if defined(_WIN32) + _tzset(); +# else + tzset(); +# endif /* _WIN32 */ + + JS::ResetTimeZone(); + +#endif /* __wasi__ */ + args.rval().setUndefined(); + return true; +} + +static bool GetCoreCount(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 0) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + args.rval().setInt32(GetCPUCount()); + return true; +} + +static bool GetDefaultLocale(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 0) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + UniqueChars locale = JS_GetDefaultLocale(cx); + if (!locale) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DEFAULT_LOCALE_ERROR); + return false; + } + + return ReturnStringCopy(cx, args, locale.get()); +} + +static bool SetDefaultLocale(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (args.length() != 1) { + ReportUsageErrorASCII(cx, callee, "Wrong number of arguments"); + return false; + } + + if (!args[0].isString() && !args[0].isUndefined()) { + ReportUsageErrorASCII(cx, callee, + "First argument should be a string or undefined"); + return false; + } + + if (args[0].isString() && !args[0].toString()->empty()) { + Rooted<JSLinearString*> str(cx, args[0].toString()->ensureLinear(cx)); + if (!str) { + return false; + } + + if (!StringIsAscii(str)) { + ReportUsageErrorASCII(cx, callee, + "First argument contains non-ASCII characters"); + return false; + } + + UniqueChars locale = JS_EncodeStringToASCII(cx, str); + if (!locale) { + return false; + } + + bool containsOnlyValidBCP47Characters = + mozilla::IsAsciiAlpha(locale[0]) && + std::all_of(locale.get(), locale.get() + str->length(), [](auto c) { + return mozilla::IsAsciiAlphanumeric(c) || c == '-'; + }); + + if (!containsOnlyValidBCP47Characters) { + ReportUsageErrorASCII(cx, callee, + "First argument should be a BCP47 language tag"); + return false; + } + + if (!JS_SetDefaultLocale(cx->runtime(), locale.get())) { + ReportOutOfMemory(cx); + return false; + } + } else { + JS_ResetDefaultLocale(cx->runtime()); + } + + args.rval().setUndefined(); + return true; +} + +#ifdef AFLFUZZ +static bool AflLoop(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + uint32_t max_cnt; + if (!ToUint32(cx, args.get(0), &max_cnt)) { + return false; + } + + args.rval().setBoolean(!!__AFL_LOOP(max_cnt)); + return true; +} +#endif + +static bool MonotonicNow(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + double now; + +// The std::chrono symbols are too new to be present in STL on all platforms we +// care about, so use raw POSIX clock APIs when it might be necessary. +#if defined(XP_UNIX) && !defined(XP_DARWIN) + auto ComputeNow = [](const timespec& ts) { + return ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + }; + + timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) { + // Use a monotonic clock if available. + now = ComputeNow(ts); + } else { + // Use a realtime clock as fallback. + if (clock_gettime(CLOCK_REALTIME, &ts) != 0) { + // Fail if no clock is available. + JS_ReportErrorASCII(cx, "can't retrieve system clock"); + return false; + } + + now = ComputeNow(ts); + + // Manually enforce atomicity on a non-monotonic clock. + { + static mozilla::Atomic<bool, mozilla::ReleaseAcquire> spinLock; + while (!spinLock.compareExchange(false, true)) { + continue; + } + + static double lastNow = -FLT_MAX; + now = lastNow = std::max(now, lastNow); + + spinLock = false; + } + } +#else + using std::chrono::duration_cast; + using std::chrono::milliseconds; + using std::chrono::steady_clock; + now = duration_cast<milliseconds>(steady_clock::now().time_since_epoch()) + .count(); +#endif // XP_UNIX && !XP_DARWIN + + args.rval().setNumber(now); + return true; +} + +static bool TimeSinceCreation(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + double when = + (mozilla::TimeStamp::Now() - mozilla::TimeStamp::ProcessCreation()) + .ToMilliseconds(); + args.rval().setNumber(when); + return true; +} + +static bool GetInnerMostEnvironmentObject(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + FrameIter iter(cx); + if (iter.done()) { + args.rval().setNull(); + return true; + } + + args.rval().setObjectOrNull(iter.environmentChain(cx)); + return true; +} + +static bool GetEnclosingEnvironmentObject(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "getEnclosingEnvironmentObject", 1)) { + return false; + } + + if (!args[0].isObject()) { + args.rval().setUndefined(); + return true; + } + + JSObject* envObj = &args[0].toObject(); + + if (envObj->is<EnvironmentObject>()) { + EnvironmentObject* env = &envObj->as<EnvironmentObject>(); + args.rval().setObject(env->enclosingEnvironment()); + return true; + } + + if (envObj->is<DebugEnvironmentProxy>()) { + DebugEnvironmentProxy* envProxy = &envObj->as<DebugEnvironmentProxy>(); + args.rval().setObject(envProxy->enclosingEnvironment()); + return true; + } + + args.rval().setNull(); + return true; +} + +static bool GetEnvironmentObjectType(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "getEnvironmentObjectType", 1)) { + return false; + } + + if (!args[0].isObject()) { + args.rval().setUndefined(); + return true; + } + + JSObject* envObj = &args[0].toObject(); + + if (envObj->is<EnvironmentObject>()) { + EnvironmentObject* env = &envObj->as<EnvironmentObject>(); + JSString* str = JS_NewStringCopyZ(cx, env->typeString()); + args.rval().setString(str); + return true; + } + if (envObj->is<DebugEnvironmentProxy>()) { + DebugEnvironmentProxy* envProxy = &envObj->as<DebugEnvironmentProxy>(); + EnvironmentObject* env = &envProxy->environment(); + char buf[256] = {'\0'}; + SprintfLiteral(buf, "[DebugProxy] %s", env->typeString()); + JSString* str = JS_NewStringCopyZ(cx, buf); + args.rval().setString(str); + return true; + } + + args.rval().setUndefined(); + return true; +} + +static bool GetErrorNotes(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "getErrorNotes", 1)) { + return false; + } + + if (!args[0].isObject() || !args[0].toObject().is<ErrorObject>()) { + args.rval().setNull(); + return true; + } + + JSErrorReport* report = args[0].toObject().as<ErrorObject>().getErrorReport(); + if (!report) { + args.rval().setNull(); + return true; + } + + RootedObject notesArray(cx, CreateErrorNotesArray(cx, report)); + if (!notesArray) { + return false; + } + + args.rval().setObject(*notesArray); + return true; +} + +static bool IsConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (args.length() < 1) { + args.rval().setBoolean(false); + } else { + args.rval().setBoolean(IsConstructor(args[0])); + } + return true; +} + +static bool SetTimeResolution(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (!args.requireAtLeast(cx, "setTimeResolution", 2)) { + return false; + } + + if (!args[0].isInt32()) { + ReportUsageErrorASCII(cx, callee, "First argument must be an Int32."); + return false; + } + int32_t resolution = args[0].toInt32(); + + if (!args[1].isBoolean()) { + ReportUsageErrorASCII(cx, callee, "Second argument must be a Boolean"); + return false; + } + bool jitter = args[1].toBoolean(); + + JS::SetTimeResolutionUsec(resolution, jitter); + + args.rval().setUndefined(); + return true; +} + +static bool ScriptedCallerGlobal(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedObject obj(cx, JS::GetScriptedCallerGlobal(cx)); + if (!obj) { + args.rval().setNull(); + return true; + } + + obj = ToWindowProxyIfWindow(obj); + + if (!cx->compartment()->wrap(cx, &obj)) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +static bool ObjectGlobal(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (!args.get(0).isObject()) { + ReportUsageErrorASCII(cx, callee, "Argument must be an object"); + return false; + } + + RootedObject obj(cx, &args[0].toObject()); + if (IsCrossCompartmentWrapper(obj)) { + args.rval().setNull(); + return true; + } + + obj = ToWindowProxyIfWindow(&obj->nonCCWGlobal()); + + args.rval().setObject(*obj); + return true; +} + +static bool IsSameCompartment(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (!args.get(0).isObject() || !args.get(1).isObject()) { + ReportUsageErrorASCII(cx, callee, "Both arguments must be objects"); + return false; + } + + RootedObject obj1(cx, UncheckedUnwrap(&args[0].toObject())); + RootedObject obj2(cx, UncheckedUnwrap(&args[1].toObject())); + + args.rval().setBoolean(obj1->compartment() == obj2->compartment()); + return true; +} + +static bool FirstGlobalInCompartment(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (!args.get(0).isObject()) { + ReportUsageErrorASCII(cx, callee, "Argument must be an object"); + return false; + } + + RootedObject obj(cx, UncheckedUnwrap(&args[0].toObject())); + obj = ToWindowProxyIfWindow(GetFirstGlobalInCompartment(obj->compartment())); + + if (!cx->compartment()->wrap(cx, &obj)) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +static bool AssertCorrectRealm(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_RELEASE_ASSERT(cx->realm() == args.callee().as<JSFunction>().realm()); + args.rval().setUndefined(); + return true; +} + +static bool GlobalLexicals(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<GlobalLexicalEnvironmentObject*> globalLexical( + cx, &cx->global()->lexicalEnvironment()); + + RootedIdVector props(cx); + if (!GetPropertyKeys(cx, globalLexical, JSITER_HIDDEN, &props)) { + return false; + } + + RootedObject res(cx, JS_NewPlainObject(cx)); + if (!res) { + return false; + } + + RootedValue val(cx); + for (size_t i = 0; i < props.length(); i++) { + HandleId id = props[i]; + if (!JS_GetPropertyById(cx, globalLexical, id, &val)) { + return false; + } + if (val.isMagic(JS_UNINITIALIZED_LEXICAL)) { + continue; + } + if (!JS_DefinePropertyById(cx, res, id, val, JSPROP_ENUMERATE)) { + return false; + } + } + + args.rval().setObject(*res); + return true; +} + +static bool EncodeAsUtf8InBuffer(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "encodeAsUtf8InBuffer", 2)) { + return false; + } + + RootedObject callee(cx, &args.callee()); + + if (!args[0].isString()) { + ReportUsageErrorASCII(cx, callee, "First argument must be a String"); + return false; + } + + // Create the amounts array early so that the raw pointer into Uint8Array + // data has as short a lifetime as possible + Rooted<ArrayObject*> array(cx, NewDenseFullyAllocatedArray(cx, 2)); + if (!array) { + return false; + } + array->ensureDenseInitializedLength(0, 2); + + JSObject* obj = args[1].isObject() ? &args[1].toObject() : nullptr; + Rooted<JS::Uint8Array> view(cx, JS::Uint8Array::unwrap(obj)); + if (!view) { + ReportUsageErrorASCII(cx, callee, "Second argument must be a Uint8Array"); + return false; + } + + size_t length; + bool isSharedMemory = false; + uint8_t* data = nullptr; + { + // The hazard analysis does not track the data pointer, so it can neither + // tell that `data` is dead if ReportUsageErrorASCII is called, nor that + // its live range ends at the call to AsWritableChars(). Construct a + // temporary scope to hide from the analysis. This should really be replaced + // with a safer mechanism. + JS::AutoCheckCannotGC nogc(cx); + if (!view.isDetached()) { + data = view.get().getLengthAndData(&length, &isSharedMemory, nogc); + } + } + + if (isSharedMemory || // exclude views of SharedArrayBuffers + !data) { // exclude views of detached ArrayBuffers + ReportUsageErrorASCII( + cx, callee, + "Second argument must be an unshared, non-detached Uint8Array"); + return false; + } + + Maybe<std::tuple<size_t, size_t>> amounts = + JS_EncodeStringToUTF8BufferPartial(cx, args[0].toString(), + AsWritableChars(Span(data, length))); + if (!amounts) { + ReportOutOfMemory(cx); + return false; + } + + auto [unitsRead, bytesWritten] = *amounts; + + array->initDenseElement(0, Int32Value(AssertedCast<int32_t>(unitsRead))); + array->initDenseElement(1, Int32Value(AssertedCast<int32_t>(bytesWritten))); + + args.rval().setObject(*array); + return true; +} + +JSScript* js::TestingFunctionArgumentToScript( + JSContext* cx, HandleValue v, JSFunction** funp /* = nullptr */) { + if (v.isString()) { + // To convert a string to a script, compile it. Parse it as an ES6 Program. + Rooted<JSString*> str(cx, v.toString()); + AutoStableStringChars linearChars(cx); + if (!linearChars.initTwoByte(cx, str)) { + return nullptr; + } + SourceText<char16_t> source; + if (!source.initMaybeBorrowed(cx, linearChars)) { + return nullptr; + } + + CompileOptions options(cx); + return JS::Compile(cx, options, source); + } + + RootedFunction fun(cx, JS_ValueToFunction(cx, v)); + if (!fun) { + return nullptr; + } + + if (!fun->isInterpreted()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TESTING_SCRIPTS_ONLY); + return nullptr; + } + + JSScript* script = JSFunction::getOrCreateScript(cx, fun); + if (!script) { + return nullptr; + } + + if (funp) { + *funp = fun; + } + + return script; +} + +static bool BaselineCompile(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + RootedScript script(cx); + if (args.length() == 0) { + NonBuiltinScriptFrameIter iter(cx); + if (iter.done()) { + ReportUsageErrorASCII(cx, callee, + "no script argument and no script caller"); + return false; + } + script = iter.script(); + } else { + script = TestingFunctionArgumentToScript(cx, args[0]); + if (!script) { + return false; + } + } + + bool forceDebug = false; + if (args.length() > 1) { + if (args.length() > 2) { + ReportUsageErrorASCII(cx, callee, "too many arguments"); + return false; + } + if (!args[1].isBoolean() && !args[1].isUndefined()) { + ReportUsageErrorASCII( + cx, callee, "forceDebugInstrumentation argument should be boolean"); + return false; + } + forceDebug = ToBoolean(args[1]); + } + + const char* returnedStr = nullptr; + do { + // In order to check for differential behaviour, baselineCompile should have + // the same output whether --no-baseline is used or not. + if (js::SupportDifferentialTesting()) { + returnedStr = "skipped (differential testing)"; + break; + } + + AutoRealm ar(cx, script); + if (script->hasBaselineScript()) { + if (forceDebug && !script->baselineScript()->hasDebugInstrumentation()) { + // There isn't an easy way to do this for a script that might be on + // stack right now. See + // js::jit::RecompileOnStackBaselineScriptsForDebugMode. + ReportUsageErrorASCII( + cx, callee, "unsupported case: recompiling script for debug mode"); + return false; + } + + args.rval().setUndefined(); + return true; + } + + if (!jit::IsBaselineJitEnabled(cx)) { + returnedStr = "baseline disabled"; + break; + } + if (!script->canBaselineCompile()) { + returnedStr = "can't compile"; + break; + } + if (!cx->realm()->ensureJitRealmExists(cx)) { + return false; + } + + jit::MethodStatus status = jit::BaselineCompile(cx, script, forceDebug); + switch (status) { + case jit::Method_Error: + return false; + case jit::Method_CantCompile: + returnedStr = "can't compile"; + break; + case jit::Method_Skipped: + returnedStr = "skipped"; + break; + case jit::Method_Compiled: + args.rval().setUndefined(); + } + } while (false); + + if (returnedStr) { + return ReturnStringCopy(cx, args, returnedStr); + } + + return true; +} + +static bool ClearKeptObjects(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + JS::ClearKeptObjects(cx); + args.rval().setUndefined(); + return true; +} + +static bool NumberToDouble(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "numberToDouble", 1)) { + return false; + } + + if (!args[0].isNumber()) { + RootedObject callee(cx, &args.callee()); + ReportUsageErrorASCII(cx, callee, "argument must be a number"); + return false; + } + + args.rval().setDouble(args[0].toNumber()); + return true; +} + +static bool GetICUOptions(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + RootedObject info(cx, JS_NewPlainObject(cx)); + if (!info) { + return false; + } + +#ifdef JS_HAS_INTL_API + RootedString str(cx); + + str = NewStringCopy<CanGC>(cx, mozilla::intl::ICU4CLibrary::GetVersion()); + if (!str || !JS_DefineProperty(cx, info, "version", str, JSPROP_ENUMERATE)) { + return false; + } + + str = NewStringCopy<CanGC>(cx, mozilla::intl::String::GetUnicodeVersion()); + if (!str || !JS_DefineProperty(cx, info, "unicode", str, JSPROP_ENUMERATE)) { + return false; + } + + str = NewStringCopyZ<CanGC>(cx, mozilla::intl::Locale::GetDefaultLocale()); + if (!str || !JS_DefineProperty(cx, info, "locale", str, JSPROP_ENUMERATE)) { + return false; + } + + auto tzdataVersion = mozilla::intl::TimeZone::GetTZDataVersion(); + if (tzdataVersion.isErr()) { + intl::ReportInternalError(cx, tzdataVersion.unwrapErr()); + return false; + } + + str = NewStringCopy<CanGC>(cx, tzdataVersion.unwrap()); + if (!str || !JS_DefineProperty(cx, info, "tzdata", str, JSPROP_ENUMERATE)) { + return false; + } + + intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buf(cx); + + if (auto ok = DateTimeInfo::timeZoneId(DateTimeInfo::ShouldRFP::No, buf); + ok.isErr()) { + intl::ReportInternalError(cx, ok.unwrapErr()); + return false; + } + + str = buf.toString(cx); + if (!str || !JS_DefineProperty(cx, info, "timezone", str, JSPROP_ENUMERATE)) { + return false; + } + + if (auto ok = mozilla::intl::TimeZone::GetHostTimeZone(buf); ok.isErr()) { + intl::ReportInternalError(cx, ok.unwrapErr()); + return false; + } + + str = buf.toString(cx); + if (!str || + !JS_DefineProperty(cx, info, "host-timezone", str, JSPROP_ENUMERATE)) { + return false; + } +#endif + + args.rval().setObject(*info); + return true; +} + +static bool GetAvailableLocalesOf(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (!args.requireAtLeast(cx, "getAvailableLocalesOf", 1)) { + return false; + } + + HandleValue arg = args[0]; + if (!arg.isString()) { + ReportUsageErrorASCII(cx, callee, "First argument must be a string"); + return false; + } + + ArrayObject* result; +#ifdef JS_HAS_INTL_API + using SupportedLocaleKind = js::intl::SharedIntlData::SupportedLocaleKind; + + SupportedLocaleKind kind; + { + JSLinearString* typeStr = arg.toString()->ensureLinear(cx); + if (!typeStr) { + return false; + } + + if (StringEqualsLiteral(typeStr, "Collator")) { + kind = SupportedLocaleKind::Collator; + } else if (StringEqualsLiteral(typeStr, "DateTimeFormat")) { + kind = SupportedLocaleKind::DateTimeFormat; + } else if (StringEqualsLiteral(typeStr, "DisplayNames")) { + kind = SupportedLocaleKind::DisplayNames; + } else if (StringEqualsLiteral(typeStr, "ListFormat")) { + kind = SupportedLocaleKind::ListFormat; + } else if (StringEqualsLiteral(typeStr, "NumberFormat")) { + kind = SupportedLocaleKind::NumberFormat; + } else if (StringEqualsLiteral(typeStr, "PluralRules")) { + kind = SupportedLocaleKind::PluralRules; + } else if (StringEqualsLiteral(typeStr, "RelativeTimeFormat")) { + kind = SupportedLocaleKind::RelativeTimeFormat; + } else { + ReportUsageErrorASCII(cx, callee, "Unsupported Intl constructor name"); + return false; + } + } + + intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + result = sharedIntlData.availableLocalesOf(cx, kind); +#else + result = NewDenseEmptyArray(cx); +#endif + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +static bool IsSmallFunction(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + RootedObject callee(cx, &args.callee()); + + if (!args.requireAtLeast(cx, "IsSmallFunction", 1)) { + return false; + } + + HandleValue arg = args[0]; + if (!arg.isObject() || !arg.toObject().is<JSFunction>()) { + ReportUsageErrorASCII(cx, callee, "First argument must be a function"); + return false; + } + + RootedFunction fun(cx, &args[0].toObject().as<JSFunction>()); + if (!fun->isInterpreted()) { + ReportUsageErrorASCII(cx, callee, + "First argument must be an interpreted function"); + return false; + } + + JSScript* script = JSFunction::getOrCreateScript(cx, fun); + if (!script) { + return false; + } + + args.rval().setBoolean(jit::JitOptions.isSmallFunction(script)); + return true; +} + +static bool PCCountProfiling_Start(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JS::StartPCCountProfiling(cx); + + args.rval().setUndefined(); + return true; +} + +static bool PCCountProfiling_Stop(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JS::StopPCCountProfiling(cx); + + args.rval().setUndefined(); + return true; +} + +static bool PCCountProfiling_Purge(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JS::PurgePCCounts(cx); + + args.rval().setUndefined(); + return true; +} + +static bool PCCountProfiling_ScriptCount(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + size_t length = JS::GetPCCountScriptCount(cx); + + args.rval().setNumber(double(length)); + return true; +} + +static bool PCCountProfiling_ScriptSummary(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "summary", 1)) { + return false; + } + + uint32_t index; + if (!JS::ToUint32(cx, args[0], &index)) { + return false; + } + + JSString* str = JS::GetPCCountScriptSummary(cx, index); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static bool PCCountProfiling_ScriptContents(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.requireAtLeast(cx, "contents", 1)) { + return false; + } + + uint32_t index; + if (!JS::ToUint32(cx, args[0], &index)) { + return false; + } + + JSString* str = JS::GetPCCountScriptContents(cx, index); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static bool NukeCCW(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1 || !args[0].isObject() || + !IsCrossCompartmentWrapper(&args[0].toObject())) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_ARGS, + "nukeCCW"); + return false; + } + + NukeCrossCompartmentWrapper(cx, &args[0].toObject()); + args.rval().setUndefined(); + return true; +} + +static bool FdLibM_Pow(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + double x; + if (!JS::ToNumber(cx, args.get(0), &x)) { + return false; + } + + double y; + if (!JS::ToNumber(cx, args.get(1), &y)) { + return false; + } + + // Because C99 and ECMA specify different behavior for pow(), we need to wrap + // the fdlibm call to make it ECMA compliant. + if (!std::isfinite(y) && (x == 1.0 || x == -1.0)) { + args.rval().setNaN(); + } else { + args.rval().setDouble(fdlibm::pow(x, y)); + } + return true; +} + +// clang-format off +static const JSFunctionSpecWithHelp TestingFunctions[] = { + JS_FN_HELP("gc", ::GC, 0, 0, +"gc([obj] | 'zone' [, ('shrinking' | 'last-ditch') ])", +" Run the garbage collector.\n" +" The first parameter describes which zones to collect: if an object is\n" +" given, GC only its zone. If 'zone' is given, GC any zones that were\n" +" scheduled via schedulegc.\n" +" The second parameter is optional and may be 'shrinking' to perform a\n" +" shrinking GC or 'last-ditch' for a shrinking, last-ditch GC."), + + JS_FN_HELP("minorgc", ::MinorGC, 0, 0, +"minorgc([aboutToOverflow])", +" Run a minor collector on the Nursery. When aboutToOverflow is true, marks\n" +" the store buffer as about-to-overflow before collecting."), + + JS_FN_HELP("maybegc", ::MaybeGC, 0, 0, +"maybegc()", +" Hint to the engine that now is an ok time to run the garbage collector.\n"), + + JS_FN_HELP("gcparam", GCParameter, 2, 0, +"gcparam(name [, value])", +" Wrapper for JS_[GS]etGCParameter. The name is one of:" GC_PARAMETER_ARGS_LIST), + + JS_FN_HELP("hasDisassembler", HasDisassembler, 0, 0, +"hasDisassembler()", +" Return true if a disassembler is present (for disnative and wasmDis)."), + + JS_FN_HELP("disnative", DisassembleNative, 2, 0, +"disnative(fun,[path])", +" Disassemble a function into its native code. Optionally write the native code bytes to a file on disk.\n"), + + JS_FN_HELP("relazifyFunctions", RelazifyFunctions, 0, 0, +"relazifyFunctions(...)", +" Perform a GC and allow relazification of functions. Accepts the same\n" +" arguments as gc()."), + + JS_FN_HELP("getBuildConfiguration", GetBuildConfiguration, 0, 0, +"getBuildConfiguration()", +" Return an object describing some of the configuration options SpiderMonkey\n" +" was built with."), + + JS_FN_HELP("getRealmConfiguration", GetRealmConfiguration, 0, 0, +"getRealmConfiguration()", +" Return an object describing some of the runtime options SpiderMonkey\n" +" is running with."), + + JS_FN_HELP("isLcovEnabled", ::IsLCovEnabled, 0, 0, +"isLcovEnabled()", +" Return true if JS LCov support is enabled."), + + JS_FN_HELP("trialInline", TrialInline, 0, 0, +"trialInline()", +" Perform trial-inlining for the caller's frame if it's a BaselineFrame."), + + JS_FN_HELP("hasChild", HasChild, 0, 0, +"hasChild(parent, child)", +" Return true if |child| is a child of |parent|, as determined by a call to\n" +" TraceChildren"), + + JS_FN_HELP("setSavedStacksRNGState", SetSavedStacksRNGState, 1, 0, +"setSavedStacksRNGState(seed)", +" Set this compartment's SavedStacks' RNG state.\n"), + + JS_FN_HELP("getSavedFrameCount", GetSavedFrameCount, 0, 0, +"getSavedFrameCount()", +" Return the number of SavedFrame instances stored in this compartment's\n" +" SavedStacks cache."), + + JS_FN_HELP("clearSavedFrames", ClearSavedFrames, 0, 0, +"clearSavedFrames()", +" Empty the current compartment's cache of SavedFrame objects, so that\n" +" subsequent stack captures allocate fresh objects to represent frames.\n" +" Clear the current stack's LiveSavedFrameCaches."), + + JS_FN_HELP("saveStack", SaveStack, 0, 0, +"saveStack([maxDepth [, compartment]])", +" Capture a stack. If 'maxDepth' is given, capture at most 'maxDepth' number\n" +" of frames. If 'compartment' is given, allocate the js::SavedFrame instances\n" +" with the given object's compartment."), + + JS_FN_HELP("captureFirstSubsumedFrame", CaptureFirstSubsumedFrame, 1, 0, +"saveStack(object [, shouldIgnoreSelfHosted = true]])", +" Capture a stack back to the first frame whose principals are subsumed by the\n" +" object's compartment's principals. If 'shouldIgnoreSelfHosted' is given,\n" +" control whether self-hosted frames are considered when checking principals."), + + JS_FN_HELP("callFunctionFromNativeFrame", CallFunctionFromNativeFrame, 1, 0, +"callFunctionFromNativeFrame(function)", +" Call 'function' with a (C++-)native frame on stack.\n" +" Required for testing that SaveStack properly handles native frames."), + + JS_FN_HELP("callFunctionWithAsyncStack", CallFunctionWithAsyncStack, 0, 0, +"callFunctionWithAsyncStack(function, stack, asyncCause)", +" Call 'function', using the provided stack as the async stack responsible\n" +" for the call, and propagate its return value or the exception it throws.\n" +" The function is called with no arguments, and 'this' is 'undefined'. The\n" +" specified |asyncCause| is attached to the provided stack frame."), + + JS_FN_HELP("enableTrackAllocations", EnableTrackAllocations, 0, 0, +"enableTrackAllocations()", +" Start capturing the JS stack at every allocation. Note that this sets an\n" +" object metadata callback that will override any other object metadata\n" +" callback that may be set."), + + JS_FN_HELP("disableTrackAllocations", DisableTrackAllocations, 0, 0, +"disableTrackAllocations()", +" Stop capturing the JS stack at every allocation."), + + JS_FN_HELP("setTestFilenameValidationCallback", SetTestFilenameValidationCallback, 0, 0, +"setTestFilenameValidationCallback()", +" Set the filename validation callback to a callback that accepts only\n" +" filenames starting with 'safe' or (only in system realms) 'system'."), + + JS_FN_HELP("newObjectWithAddPropertyHook", NewObjectWithAddPropertyHook, 0, 0, +"newObjectWithAddPropertyHook()", +" Returns a new object with an addProperty JSClass hook. This hook\n" +" increments the value of the _propertiesAdded data property on the object\n" +" when a new property is added."), + + JS_FN_HELP("newObjectWithCallHook", NewObjectWithCallHook, 0, 0, +"newObjectWithCallHook()", +" Returns a new object with call/construct JSClass hooks. These hooks return\n" +" a new object that contains the Values supplied by the caller."), + + JS_FN_HELP("newObjectWithManyReservedSlots", NewObjectWithManyReservedSlots, 0, 0, +"newObjectWithManyReservedSlots()", +" Returns a new object with many reserved slots. The slots are initialized to int32\n" +" values. checkObjectWithManyReservedSlots can be used to check the slots still\n" +" hold these values."), + + JS_FN_HELP("checkObjectWithManyReservedSlots", CheckObjectWithManyReservedSlots, 1, 0, +"checkObjectWithManyReservedSlots(obj)", +" Checks the reserved slots set by newObjectWithManyReservedSlots still hold the expected\n" +" values."), + + JS_FN_HELP("getWatchtowerLog", GetWatchtowerLog, 0, 0, +"getWatchtowerLog()", +" Returns the Watchtower log recording object changes for objects for which\n" +" addWatchtowerTarget was called. The internal log is cleared. The return\n" +" value is an array of plain objects with the following properties:\n" +" - kind: a string describing the kind of mutation, for example \"add-prop\"\n" +" - object: the object being mutated\n" +" - extra: an extra value, for example the name of the property being added"), + + JS_FN_HELP("addWatchtowerTarget", AddWatchtowerTarget, 1, 0, +"addWatchtowerTarget(object)", +" Invoke the watchtower callback for changes to this object."), + + JS_FN_HELP("newString", NewString, 2, 0, +"newString(str[, options])", +" Copies str's chars and returns a new string. Valid options:\n" +" \n" +" - tenured: allocate directly into the tenured heap.\n" +" \n" +" - twoByte: create a \"two byte\" string, not a latin1 string, regardless of the\n" +" input string's characters. Latin1 will be used by default if possible\n" +" (again regardless of the input string.)\n" +" \n" +" - external: create an external string. External strings are always twoByte and\n" +" tenured.\n" +" \n" +" - maybeExternal: create an external string, unless the data fits within an\n" +" inline string. Inline strings may be nursery-allocated."), + + JS_FN_HELP("ensureLinearString", EnsureLinearString, 1, 0, +"ensureLinearString(str)", +" Ensures str is a linear (non-rope) string and returns it."), + + JS_FN_HELP("representativeStringArray", RepresentativeStringArray, 0, 0, +"representativeStringArray()", +" Returns an array of strings that represent the various internal string\n" +" types and character encodings."), + +#if defined(DEBUG) || defined(JS_OOM_BREAKPOINT) + + JS_FN_HELP("oomThreadTypes", OOMThreadTypes, 0, 0, +"oomThreadTypes()", +" Get the number of thread types that can be used as an argument for\n" +" oomAfterAllocations() and oomAtAllocation()."), + + JS_FN_HELP("oomAfterAllocations", OOMAfterAllocations, 2, 0, +"oomAfterAllocations(count [,threadType])", +" After 'count' js_malloc memory allocations, fail every following allocation\n" +" (return nullptr). The optional thread type limits the effect to the\n" +" specified type of helper thread."), + + JS_FN_HELP("oomAtAllocation", OOMAtAllocation, 2, 0, +"oomAtAllocation(count [,threadType])", +" After 'count' js_malloc memory allocations, fail the next allocation\n" +" (return nullptr). The optional thread type limits the effect to the\n" +" specified type of helper thread."), + + JS_FN_HELP("resetOOMFailure", ResetOOMFailure, 0, 0, +"resetOOMFailure()", +" Remove the allocation failure scheduled by either oomAfterAllocations() or\n" +" oomAtAllocation() and return whether any allocation had been caused to fail."), + + JS_FN_HELP("oomTest", OOMTest, 0, 0, +"oomTest(function, [expectExceptionOnFailure = true | options])", +" Test that the passed function behaves correctly under OOM conditions by\n" +" repeatedly executing it and simulating allocation failure at successive\n" +" allocations until the function completes without seeing a failure.\n" +" By default this tests that an exception is raised if execution fails, but\n" +" this can be disabled by passing false as the optional second parameter.\n" +" This is also disabled when --fuzzing-safe is specified.\n" +" Alternatively an object can be passed to set the following options:\n" +" expectExceptionOnFailure: bool - as described above.\n" +" keepFailing: bool - continue to fail after first simulated failure.\n" +"\n" +" WARNING: By design, oomTest assumes the test-function follows the same\n" +" code path each time it is called, right up to the point where OOM occurs.\n" +" If on iteration 70 it finishes and caches a unit of work that saves 65\n" +" allocations the next time we run, then the subsequent 65 allocation\n" +" points will go untested.\n" +"\n" +" Things in this category include lazy parsing and baseline compilation,\n" +" so it is very easy to accidentally write an oomTest that only tests one\n" +" or the other of those, and not the functionality you meant to test!\n" +" To avoid lazy parsing, call the test function once first before passing\n" +" it to oomTest. The jits can be disabled via the test harness.\n"), + + JS_FN_HELP("stackTest", StackTest, 0, 0, +"stackTest(function, [expectExceptionOnFailure = true])", +" This function behaves exactly like oomTest with the difference that\n" +" instead of simulating regular OOM conditions, it simulates the engine\n" +" running out of stack space (failing recursion check).\n" +"\n" +" See the WARNING in help('oomTest').\n"), + + JS_FN_HELP("interruptTest", InterruptTest, 0, 0, +"interruptTest(function)", +" This function simulates interrupts similar to how oomTest simulates OOM conditions." +"\n" +" See the WARNING in help('oomTest').\n"), + +#endif // defined(DEBUG) || defined(JS_OOM_BREAKPOINT) + + JS_FN_HELP("newRope", NewRope, 3, 0, +"newRope(left, right[, options])", +" Creates a rope with the given left/right strings.\n" +" Available options:\n" +" nursery: bool - force the string to be created in/out of the nursery, if possible.\n"), + + JS_FN_HELP("isRope", IsRope, 1, 0, +"isRope(str)", +" Returns true if the parameter is a rope"), + + JS_FN_HELP("settlePromiseNow", SettlePromiseNow, 1, 0, +"settlePromiseNow(promise)", +" 'Settle' a 'promise' immediately. This just marks the promise as resolved\n" +" with a value of `undefined` and causes the firing of any onPromiseSettled\n" +" hooks set on Debugger instances that are observing the given promise's\n" +" global as a debuggee."), + JS_FN_HELP("getWaitForAllPromise", GetWaitForAllPromise, 1, 0, +"getWaitForAllPromise(densePromisesArray)", +" Calls the 'GetWaitForAllPromise' JSAPI function and returns the result\n" +" Promise."), +JS_FN_HELP("resolvePromise", ResolvePromise, 2, 0, +"resolvePromise(promise, resolution)", +" Resolve a Promise by calling the JSAPI function JS::ResolvePromise."), +JS_FN_HELP("rejectPromise", RejectPromise, 2, 0, +"rejectPromise(promise, reason)", +" Reject a Promise by calling the JSAPI function JS::RejectPromise."), + + JS_FN_HELP("makeFinalizeObserver", MakeFinalizeObserver, 0, 0, +"makeFinalizeObserver()", +" Get a special object whose finalization increases the counter returned\n" +" by the finalizeCount function."), + + JS_FN_HELP("finalizeCount", FinalizeCount, 0, 0, +"finalizeCount()", +" Return the current value of the finalization counter that is incremented\n" +" each time an object returned by the makeFinalizeObserver is finalized."), + + JS_FN_HELP("resetFinalizeCount", ResetFinalizeCount, 0, 0, +"resetFinalizeCount()", +" Reset the value returned by finalizeCount()."), + + JS_FN_HELP("gcPreserveCode", GCPreserveCode, 0, 0, +"gcPreserveCode()", +" Preserve JIT code during garbage collections."), + +#ifdef JS_GC_ZEAL + JS_FN_HELP("gczeal", GCZeal, 2, 0, +"gczeal(mode, [frequency])", +gc::ZealModeHelpText), + + JS_FN_HELP("unsetgczeal", UnsetGCZeal, 2, 0, +"unsetgczeal(mode)", +" Turn off a single zeal mode set with gczeal() and don't finish any ongoing\n" +" collection that may be happening."), + + JS_FN_HELP("schedulegc", ScheduleGC, 1, 0, +"schedulegc([num])", +" If num is given, schedule a GC after num allocations.\n" +" Returns the number of allocations before the next trigger."), + + JS_FN_HELP("selectforgc", SelectForGC, 0, 0, +"selectforgc(obj1, obj2, ...)", +" Schedule the given objects to be marked in the next GC slice."), + + JS_FN_HELP("verifyprebarriers", VerifyPreBarriers, 0, 0, +"verifyprebarriers()", +" Start or end a run of the pre-write barrier verifier."), + + JS_FN_HELP("verifypostbarriers", VerifyPostBarriers, 0, 0, +"verifypostbarriers()", +" Does nothing (the post-write barrier verifier has been remove)."), + + JS_FN_HELP("currentgc", CurrentGC, 0, 0, +"currentgc()", +" Report various information about the currently running incremental GC,\n" +" if one is running."), + + JS_FN_HELP("deterministicgc", DeterministicGC, 1, 0, +"deterministicgc(true|false)", +" If true, only allow determinstic GCs to run."), + + JS_FN_HELP("dumpGCArenaInfo", DumpGCArenaInfo, 0, 0, +"dumpGCArenaInfo()", +" Prints information about the different GC things and how they are arranged\n" +" in arenas.\n"), + + JS_FN_HELP("setMarkStackLimit", SetMarkStackLimit, 1, 0, +"markStackLimit(limit)", +" Sets a limit on the number of words used for the mark stack. Used to test OOM" +" handling during marking.\n"), + +#endif + + JS_FN_HELP("gcstate", GCState, 0, 0, +"gcstate([obj])", +" Report the global GC state, or the GC state for the zone containing |obj|."), + + JS_FN_HELP("schedulezone", ScheduleZoneForGC, 1, 0, +"schedulezone([obj | string])", +" If obj is given, schedule a GC of obj's zone.\n" +" If string is given, schedule a GC of the string's zone if possible."), + + JS_FN_HELP("startgc", StartGC, 1, 0, +"startgc([n [, 'shrinking']])", +" Start an incremental GC and run a slice that processes about n objects.\n" +" If 'shrinking' is passesd as the optional second argument, perform a\n" +" shrinking GC rather than a normal GC. If no zones have been selected with\n" +" schedulezone(), a full GC will be performed."), + + JS_FN_HELP("finishgc", FinishGC, 0, 0, +"finishgc()", +" Finish an in-progress incremental GC, if none is running then do nothing."), + + JS_FN_HELP("gcslice", GCSlice, 1, 0, +"gcslice([n [, options]])", +" Start or continue an an incremental GC, running a slice that processes\n" +" about n objects. Takes an optional options object, which may contain the\n" +" following properties:\n" +" dontStart: do not start a new incremental GC if one is not already\n" +" running"), + + JS_FN_HELP("abortgc", AbortGC, 1, 0, +"abortgc()", +" Abort the current incremental GC."), + + JS_FN_HELP("setMallocMaxDirtyPageModifier", SetMallocMaxDirtyPageModifier, 1, 0, +"setMallocMaxDirtyPageModifier(value)", +" Change the maximum size of jemalloc's page cache. The value should be between\n" +" -5 and 16 (inclusive). See moz_set_max_dirty_page_modifier.\n"), + + JS_FN_HELP("fullcompartmentchecks", FullCompartmentChecks, 1, 0, +"fullcompartmentchecks(true|false)", +" If true, check for compartment mismatches before every GC."), + + JS_FN_HELP("nondeterministicGetWeakMapKeys", NondeterministicGetWeakMapKeys, 1, 0, +"nondeterministicGetWeakMapKeys(weakmap)", +" Return an array of the keys in the given WeakMap."), + + JS_FN_HELP("internalConst", InternalConst, 1, 0, +"internalConst(name)", +" Query an internal constant for the engine. See InternalConst source for\n" +" the list of constant names."), + + JS_FN_HELP("isProxy", IsProxy, 1, 0, +"isProxy(obj)", +" If true, obj is a proxy of some sort"), + + JS_FN_HELP("dumpHeap", DumpHeap, 1, 0, +"dumpHeap([filename])", +" Dump reachable and unreachable objects to the named file, or to stdout. Objects\n" +" in the nursery are ignored, so if you wish to include them, consider calling\n" +" minorgc() first."), + + JS_FN_HELP("terminate", Terminate, 0, 0, +"terminate()", +" Terminate JavaScript execution, as if we had run out of\n" +" memory or been terminated by the slow script dialog."), + + JS_FN_HELP("readGeckoProfilingStack", ReadGeckoProfilingStack, 0, 0, +"readGeckoProfilingStack()", +" Reads the JIT/Wasm stack using ProfilingFrameIterator. Skips non-JIT/Wasm frames."), + + JS_FN_HELP("readGeckoInterpProfilingStack", ReadGeckoInterpProfilingStack, 0, 0, +"readGeckoInterpProfilingStack()", +" Reads the C++ interpreter profiling stack. Skips JIT/Wasm frames."), + + JS_FN_HELP("enableOsiPointRegisterChecks", EnableOsiPointRegisterChecks, 0, 0, +"enableOsiPointRegisterChecks()", +" Emit extra code to verify live regs at the start of a VM call are not\n" +" modified before its OsiPoint."), + + JS_FN_HELP("displayName", DisplayName, 1, 0, +"displayName(fn)", +" Gets the display name for a function, which can possibly be a guessed or\n" +" inferred name based on where the function was defined. This can be\n" +" different from the 'name' property on the function."), + + JS_FN_HELP("isAsmJSCompilationAvailable", IsAsmJSCompilationAvailable, 0, 0, +"isAsmJSCompilationAvailable", +" Returns whether asm.js compilation is currently available or whether it is disabled\n" +" (e.g., by the debugger)."), + + JS_FN_HELP("getJitCompilerOptions", GetJitCompilerOptions, 0, 0, +"getJitCompilerOptions()", +" Return an object describing some of the JIT compiler options.\n"), + + JS_FN_HELP("isAsmJSModule", IsAsmJSModule, 1, 0, +"isAsmJSModule(fn)", +" Returns whether the given value is a function containing \"use asm\" that has been\n" +" validated according to the asm.js spec."), + + JS_FN_HELP("isAsmJSFunction", IsAsmJSFunction, 1, 0, +"isAsmJSFunction(fn)", +" Returns whether the given value is a nested function in an asm.js module that has been\n" +" both compile- and link-time validated."), + + JS_FN_HELP("isAvxPresent", IsAvxPresent, 0, 0, +"isAvxPresent([minVersion])", +" Returns whether AVX is present and enabled. If minVersion specified,\n" +" use 1 - to check if AVX is enabled (default), 2 - if AVX2 is enabled."), + + JS_FN_HELP("wasmIsSupported", WasmIsSupported, 0, 0, +"wasmIsSupported()", +" Returns a boolean indicating whether WebAssembly is supported on the current device."), + + JS_FN_HELP("wasmIsSupportedByHardware", WasmIsSupportedByHardware, 0, 0, +"wasmIsSupportedByHardware()", +" Returns a boolean indicating whether WebAssembly is supported on the current hardware (regardless of whether we've enabled support)."), + + JS_FN_HELP("wasmDebuggingEnabled", WasmDebuggingEnabled, 0, 0, +"wasmDebuggingEnabled()", +" Returns a boolean indicating whether WebAssembly debugging is supported on the current device;\n" +" returns false also if WebAssembly is not supported"), + + JS_FN_HELP("wasmStreamingEnabled", WasmStreamingEnabled, 0, 0, +"wasmStreamingEnabled()", +" Returns a boolean indicating whether WebAssembly caching is supported by the runtime."), + + JS_FN_HELP("wasmCachingEnabled", WasmCachingEnabled, 0, 0, +"wasmCachingEnabled()", +" Returns a boolean indicating whether WebAssembly caching is supported by the runtime."), + + JS_FN_HELP("wasmHugeMemorySupported", WasmHugeMemorySupported, 0, 0, +"wasmHugeMemorySupported()", +" Returns a boolean indicating whether WebAssembly supports using a large" +" virtual memory reservation in order to elide bounds checks on this platform."), + + JS_FN_HELP("wasmMaxMemoryPages", WasmMaxMemoryPages, 1, 0, +"wasmMaxMemoryPages(indexType)", +" Returns an int with the maximum number of pages that can be allocated to a memory." +" This is an implementation artifact that does depend on the index type, the hardware," +" the operating system, the build configuration, and flags. The result is constant for" +" a given combination of those; there is no guarantee that that size allocation will" +" always succeed, only that it can succeed in principle. The indexType is a string," +" 'i32' or 'i64'."), + +#define WASM_FEATURE(NAME, ...) \ + JS_FN_HELP("wasm" #NAME "Enabled", Wasm##NAME##Enabled, 0, 0, \ +"wasm" #NAME "Enabled()", \ +" Returns a boolean indicating whether the WebAssembly " #NAME " proposal is enabled."), +JS_FOR_WASM_FEATURES(WASM_FEATURE, WASM_FEATURE, WASM_FEATURE) +#undef WASM_FEATURE + + JS_FN_HELP("wasmThreadsEnabled", WasmThreadsEnabled, 0, 0, +"wasmThreadsEnabled()", +" Returns a boolean indicating whether the WebAssembly threads proposal is\n" +" supported on the current device."), + + JS_FN_HELP("wasmSimdEnabled", WasmSimdEnabled, 0, 0, +"wasmSimdEnabled()", +" Returns a boolean indicating whether WebAssembly SIMD proposal is\n" +" supported by the current device."), + +#if defined(ENABLE_WASM_SIMD) && defined(DEBUG) + JS_FN_HELP("wasmSimdAnalysis", WasmSimdAnalysis, 1, 0, +"wasmSimdAnalysis(...)", +" Unstable API for white-box testing.\n"), +#endif + + JS_FN_HELP("wasmGlobalFromArrayBuffer", WasmGlobalFromArrayBuffer, 2, 0, +"wasmGlobalFromArrayBuffer(type, arrayBuffer)", +" Create a WebAssembly.Global object from a provided ArrayBuffer. The type\n" +" must be POD (i32, i64, f32, f64, v128). The buffer must be the same\n" +" size as the type in bytes.\n"), + JS_FN_HELP("wasmGlobalExtractLane", WasmGlobalExtractLane, 3, 0, +"wasmGlobalExtractLane(global, laneInterp, laneIndex)", +" Extract a lane from a WebAssembly.Global object that contains a v128 value\n" +" and return it as a new WebAssembly.Global object of the appropriate type.\n" +" The supported laneInterp values are i32x4, i64x2, f32x4, and\n" +" f64x2.\n"), + JS_FN_HELP("wasmGlobalsEqual", WasmGlobalsEqual, 2, 0, +"wasmGlobalsEqual(globalA, globalB)", +" Compares two WebAssembly.Global objects for if their types and values are\n" +" equal. Mutability is not compared. Floating point values are compared for\n" +" bitwise equality, not IEEE 754 equality.\n"), + JS_FN_HELP("wasmGlobalIsNaN", WasmGlobalIsNaN, 2, 0, +"wasmGlobalIsNaN(global, flavor)", +" Compares a floating point WebAssembly.Global object for if its value is a\n" +" specific NaN flavor. Valid flavors are `arithmetic_nan` and `canonical_nan`.\n"), + JS_FN_HELP("wasmGlobalToString", WasmGlobalToString, 1, 0, +"wasmGlobalToString(global)", +" Returns a debug representation of the contents of a WebAssembly.Global\n" +" object.\n"), + JS_FN_HELP("wasmLosslessInvoke", WasmLosslessInvoke, 1, 0, +"wasmLosslessInvoke(wasmFunc, args...)", +" Invokes the provided WebAssembly function using a modified conversion\n" +" function that allows providing a param as a WebAssembly.Global and\n" +" returning a result as a WebAssembly.Global.\n"), + + JS_FN_HELP("wasmCompilersPresent", WasmCompilersPresent, 0, 0, +"wasmCompilersPresent()", +" Returns a string indicating the present wasm compilers: a comma-separated list\n" +" of 'baseline', 'ion'. A compiler is present in the executable if it is compiled\n" +" in and can generate code for the current architecture."), + + JS_FN_HELP("wasmCompileMode", WasmCompileMode, 0, 0, +"wasmCompileMode()", +" Returns a string indicating the available wasm compilers: 'baseline', 'ion',\n" +" 'baseline+ion', or 'none'. A compiler is available if it is present in the\n" +" executable and not disabled by switches or runtime conditions. At most one\n" +" baseline and one optimizing compiler can be available."), + + JS_FN_HELP("wasmBaselineDisabledByFeatures", WasmBaselineDisabledByFeatures, 0, 0, +"wasmBaselineDisabledByFeatures()", +" If some feature is enabled at compile-time or run-time that prevents baseline\n" +" from being used then this returns a truthy string describing the features that\n." +" are disabling it. Otherwise it returns false."), + + JS_FN_HELP("wasmIonDisabledByFeatures", WasmIonDisabledByFeatures, 0, 0, +"wasmIonDisabledByFeatures()", +" If some feature is enabled at compile-time or run-time that prevents Ion\n" +" from being used then this returns a truthy string describing the features that\n." +" are disabling it. Otherwise it returns false."), + + JS_FN_HELP("wasmExtractCode", WasmExtractCode, 1, 0, +"wasmExtractCode(module[, tier])", +" Extracts generated machine code from WebAssembly.Module. The tier is a string,\n" +" 'stable', 'best', 'baseline', or 'ion'; the default is 'stable'. If the request\n" +" cannot be satisfied then null is returned. If the request is 'ion' then block\n" +" until background compilation is complete."), + + JS_FN_HELP("wasmDis", WasmDisassemble, 1, 0, +"wasmDis(wasmObject[, options])\n", +" Disassembles generated machine code from an exported WebAssembly function,\n" +" or from all the functions defined in the module or instance, exported and not.\n" +" The `options` is an object with the following optional keys:\n" +" asString: boolean - if true, return a string rather than printing on stderr,\n" +" the default is false.\n" +" tier: string - one of 'stable', 'best', 'baseline', or 'ion'; the default is\n" +" 'stable'.\n" +" kinds: string - if set, and the wasmObject is a module or instance, a\n" +" comma-separated list of the following keys, the default is `Function`:\n" +" Function - functions defined in the module\n" +" InterpEntry - C++-to-wasm stubs\n" +" JitEntry - jitted-js-to-wasm stubs\n" +" ImportInterpExit - wasm-to-C++ stubs\n" +" ImportJitExit - wasm-to-jitted-JS stubs\n" +" all - all kinds, including obscure ones\n"), + + JS_FN_HELP("wasmHasTier2CompilationCompleted", WasmHasTier2CompilationCompleted, 1, 0, +"wasmHasTier2CompilationCompleted(module)", +" Returns a boolean indicating whether a given module has finished compiled code for tier2. \n" +"This will return true early if compilation isn't two-tiered. "), + + JS_FN_HELP("wasmLoadedFromCache", WasmLoadedFromCache, 1, 0, +"wasmLoadedFromCache(module)", +" Returns a boolean indicating whether a given module was deserialized directly from a\n" +" cache (as opposed to compiled from bytecode)."), + + JS_FN_HELP("wasmIntrinsicI8VecMul", WasmIntrinsicI8VecMul, 0, 0, +"wasmIntrinsicI8VecMul()", +" Returns a module that implements an i8 vector pairwise multiplication intrinsic."), + + JS_FN_HELP("largeArrayBufferSupported", LargeArrayBufferSupported, 0, 0, +"largeArrayBufferSupported()", +" Returns true if array buffers larger than 2GB can be allocated."), + + JS_FN_HELP("isLazyFunction", IsLazyFunction, 1, 0, +"isLazyFunction(fun)", +" True if fun is a lazy JSFunction."), + + JS_FN_HELP("isRelazifiableFunction", IsRelazifiableFunction, 1, 0, +"isRelazifiableFunction(fun)", +" True if fun is a JSFunction with a relazifiable JSScript."), + + JS_FN_HELP("hasSameBytecodeData", HasSameBytecodeData, 2, 0, +"hasSameBytecodeData(fun1, fun2)", +" True if fun1 and fun2 share the same copy of bytecode data. This will\n" +" delazify the function if necessary."), + + JS_FN_HELP("enableShellAllocationMetadataBuilder", EnableShellAllocationMetadataBuilder, 0, 0, +"enableShellAllocationMetadataBuilder()", +" Use ShellAllocationMetadataBuilder to supply metadata for all newly created objects."), + + JS_FN_HELP("getAllocationMetadata", GetAllocationMetadata, 1, 0, +"getAllocationMetadata(obj)", +" Get the metadata for an object."), + + JS_INLINABLE_FN_HELP("bailout", testingFunc_bailout, 0, 0, TestBailout, +"bailout()", +" Force a bailout out of ionmonkey (if running in ionmonkey)."), + + JS_FN_HELP("bailAfter", testingFunc_bailAfter, 1, 0, +"bailAfter(number)", +" Start a counter to bail once after passing the given amount of possible bailout positions in\n" +" ionmonkey.\n"), + + JS_FN_HELP("invalidate", testingFunc_invalidate, 0, 0, +"invalidate()", +" Force an immediate invalidation (if running in Warp)."), + + JS_FN_HELP("inJit", testingFunc_inJit, 0, 0, +"inJit()", +" Returns true when called within (jit-)compiled code. When jit compilation is disabled this\n" +" function returns an error string. This function returns false in all other cases.\n" +" Depending on truthiness, you should continue to wait for compilation to happen or stop execution.\n"), + + JS_FN_HELP("inIon", testingFunc_inIon, 0, 0, +"inIon()", +" Returns true when called within ion. When ion is disabled or when compilation is abnormally\n" +" slow to start, this function returns an error string. Otherwise, this function returns false.\n" +" This behaviour ensures that a falsy value means that we are not in ion, but expect a\n" +" compilation to occur in the future. Conversely, a truthy value means that we are either in\n" +" ion or that there is litle or no chance of ion ever compiling the current script."), + + JS_FN_HELP("assertJitStackInvariants", TestingFunc_assertJitStackInvariants, 0, 0, +"assertJitStackInvariants()", +" Iterates the Jit stack and check that stack invariants hold."), + + JS_FN_HELP("setIonCheckGraphCoherency", SetIonCheckGraphCoherency, 1, 0, +"setIonCheckGraphCoherency(bool)", +" Set whether Ion should perform graph consistency (DEBUG-only) assertions. These assertions\n" +" are valuable and should be generally enabled, however they can be very expensive for large\n" +" (wasm) programs."), + + JS_FN_HELP("serialize", testingFunc_serialize, 1, 0, +"serialize(data, [transferables, [policy]])", +" Serialize 'data' using JS_WriteStructuredClone. Returns a structured\n" +" clone buffer object. 'policy' may be an options hash. Valid keys:\n" +" 'SharedArrayBuffer' - either 'allow' or 'deny' (the default)\n" +" to specify whether SharedArrayBuffers may be serialized.\n" +" 'scope' - SameProcess, DifferentProcess, or\n" +" DifferentProcessForIndexedDB. Determines how some values will be\n" +" serialized. Clone buffers may only be deserialized with a compatible\n" +" scope. NOTE - For DifferentProcess/DifferentProcessForIndexedDB,\n" +" must also set SharedArrayBuffer:'deny' if data contains any shared memory\n" +" object."), + + JS_FN_HELP("deserialize", Deserialize, 1, 0, +"deserialize(clonebuffer[, opts])", +" Deserialize data generated by serialize. 'opts' may be an options hash.\n" +" Valid keys:\n" +" 'SharedArrayBuffer' - either 'allow' or 'deny' (the default)\n" +" to specify whether SharedArrayBuffers may be serialized.\n" +" 'scope', which limits the clone buffers that are considered\n" +" valid. Allowed values: ''SameProcess', 'DifferentProcess',\n" +" and 'DifferentProcessForIndexedDB'. So for example, a\n" +" DifferentProcessForIndexedDB clone buffer may be deserialized in any scope, but\n" +" a SameProcess clone buffer cannot be deserialized in a\n" +" DifferentProcess scope."), + + JS_FN_HELP("detachArrayBuffer", DetachArrayBuffer, 1, 0, +"detachArrayBuffer(buffer)", +" Detach the given ArrayBuffer object from its memory, i.e. as if it\n" +" had been transferred to a WebWorker."), + + JS_FN_HELP("makeSerializable", MakeSerializable, 1, 0, +"makeSerializable(numeric id, [behavior])", +" Make a custom serializable, transferable object. It will have a single accessor\n" +" obj.log that will give a history of all operations on all such objects in the\n" +" current thread as an array [id, action, id, action, ...] where the id\n" +" is the number passed into this function, and the action is one of:\n" +" ? - the canTransfer() hook was called.\n" +" w - the write() hook was called.\n" +" W - the writeTransfer() hook was called.\n" +" R - the readTransfer() hook was called.\n" +" r - the read() hook was called.\n" +" F - the freeTransfer() hook was called.\n" +" The `behavior` parameter can be used to force a failure during processing:\n" +" 1 - fail during readTransfer() hook\n" +" 2 - fail during read() hook\n" +" Set the log to null to clear it."), + + JS_FN_HELP("helperThreadCount", HelperThreadCount, 0, 0, +"helperThreadCount()", +" Returns the number of helper threads available for off-thread tasks."), + + JS_FN_HELP("createShapeSnapshot", CreateShapeSnapshot, 1, 0, +"createShapeSnapshot(obj)", +" Returns an object containing a shape snapshot for use with\n" +" checkShapeSnapshot.\n"), + + JS_FN_HELP("checkShapeSnapshot", CheckShapeSnapshot, 2, 0, +"checkShapeSnapshot(snapshot, [obj])", +" Check shape invariants based on the given snapshot and optional object.\n" +" If there's no object argument, the snapshot's object is used.\n"), + + JS_FN_HELP("enableShapeConsistencyChecks", EnableShapeConsistencyChecks, 0, 0, +"enableShapeConsistencyChecks()", +" Enable some slow Shape assertions.\n"), + + JS_FN_HELP("reportOutOfMemory", ReportOutOfMemory, 0, 0, +"reportOutOfMemory()", +" Report OOM, then clear the exception and return undefined. For crash testing."), + + JS_FN_HELP("throwOutOfMemory", ThrowOutOfMemory, 0, 0, +"throwOutOfMemory()", +" Throw out of memory exception, for OOM handling testing."), + + JS_FN_HELP("reportLargeAllocationFailure", ReportLargeAllocationFailure, 0, 0, +"reportLargeAllocationFailure([bytes])", +" Call the large allocation failure callback, as though a large malloc call failed,\n" +" then return undefined. In Gecko, this sends a memory pressure notification, which\n" +" can free up some memory."), + + JS_FN_HELP("findPath", FindPath, 2, 0, +"findPath(start, target)", +" Return an array describing one of the shortest paths of GC heap edges from\n" +" |start| to |target|, or |undefined| if |target| is unreachable from |start|.\n" +" Each element of the array is either of the form:\n" +" { node: <object or string>, edge: <string describing edge from node> }\n" +" if the node is a JavaScript object or value; or of the form:\n" +" { type: <string describing node>, edge: <string describing edge> }\n" +" if the node is some internal thing that is not a proper JavaScript value\n" +" (like a shape or a scope chain element). The destination of the i'th array\n" +" element's edge is the node of the i+1'th array element; the destination of\n" +" the last array element is implicitly |target|.\n"), + +#if defined(DEBUG) || defined(JS_JITSPEW) + JS_FN_HELP("dumpObject", DumpObject, 1, 0, +"dumpObject()", +" Dump an internal representation of an object."), +#endif + + JS_FN_HELP("sharedMemoryEnabled", SharedMemoryEnabled, 0, 0, +"sharedMemoryEnabled()", +" Return true if SharedArrayBuffer and Atomics are enabled"), + + JS_FN_HELP("sharedArrayRawBufferRefcount", SharedArrayRawBufferRefcount, 0, 0, +"sharedArrayRawBufferRefcount(sab)", +" Return the reference count of the SharedArrayRawBuffer object held by sab"), + +#ifdef NIGHTLY_BUILD + JS_FN_HELP("objectAddress", ObjectAddress, 1, 0, +"objectAddress(obj)", +" Return the current address of the object. For debugging only--this\n" +" address may change during a moving GC."), + + JS_FN_HELP("sharedAddress", SharedAddress, 1, 0, +"sharedAddress(obj)", +" Return the address of the shared storage of a SharedArrayBuffer."), +#endif + + JS_FN_HELP("hasInvalidatedTeleporting", HasInvalidatedTeleporting, 1, 0, +"hasInvalidatedTeleporting(obj)", +" Return true if the shape teleporting optimization has been disabled for |obj|."), + + JS_FN_HELP("evalReturningScope", EvalReturningScope, 1, 0, +"evalReturningScope(scriptStr, [global])", +" Evaluate the script in a new scope and return the scope.\n" +" If |global| is present, clone the script to |global| before executing."), + + JS_FN_HELP("backtrace", DumpBacktrace, 1, 0, +"backtrace()", +" Dump out a brief backtrace."), + + JS_FN_HELP("getBacktrace", GetBacktrace, 1, 0, +"getBacktrace([options])", +" Return the current stack as a string. Takes an optional options object,\n" +" which may contain any or all of the boolean properties:\n" +" options.args - show arguments to each function\n" +" options.locals - show local variables in each frame\n" +" options.thisprops - show the properties of the 'this' object of each frame\n"), + + JS_FN_HELP("byteSize", ByteSize, 1, 0, +"byteSize(value)", +" Return the size in bytes occupied by |value|, or |undefined| if value\n" +" is not allocated in memory.\n"), + + JS_FN_HELP("byteSizeOfScript", ByteSizeOfScript, 1, 0, +"byteSizeOfScript(f)", +" Return the size in bytes occupied by the function |f|'s JSScript.\n"), + + JS_FN_HELP("setImmutablePrototype", SetImmutablePrototype, 1, 0, +"setImmutablePrototype(obj)", +" Try to make obj's [[Prototype]] immutable, such that subsequent attempts to\n" +" change it will fail. Return true if obj's [[Prototype]] was successfully made\n" +" immutable (or if it already was immutable), false otherwise. Throws in case\n" +" of internal error, or if the operation doesn't even make sense (for example,\n" +" because the object is a revoked proxy)."), + +#ifdef DEBUG + JS_FN_HELP("dumpStringRepresentation", DumpStringRepresentation, 1, 0, +"dumpStringRepresentation(str)", +" Print a human-readable description of how the string |str| is represented.\n"), + + JS_FN_HELP("stringRepresentation", GetStringRepresentation, 1, 0, +"stringRepresentation(str)", +" Return a human-readable description of how the string |str| is represented.\n"), + +#endif + + JS_FN_HELP("allocationMarker", AllocationMarker, 0, 0, +"allocationMarker([options])", +" Return a freshly allocated object whose [[Class]] name is\n" +" \"AllocationMarker\". Such objects are allocated only by calls\n" +" to this function, never implicitly by the system, making them\n" +" suitable for use in allocation tooling tests. Takes an optional\n" +" options object which may contain the following properties:\n" +" * nursery: bool, whether to allocate the object in the nursery\n"), + + JS_FN_HELP("setGCCallback", SetGCCallback, 1, 0, +"setGCCallback({action:\"...\", options...})", +" Set the GC callback. action may be:\n" +" 'minorGC' - run a nursery collection\n" +" 'majorGC' - run a major collection, nesting up to a given 'depth'\n"), + +#ifdef DEBUG + JS_FN_HELP("enqueueMark", EnqueueMark, 1, 0, +"enqueueMark(obj|string)", +" Add an object to the queue of objects to mark at the beginning every GC. (Note\n" +" that the objects will actually be marked at the beginning of every slice, but\n" +" after the first slice they will already be marked so nothing will happen.)\n" +" \n" +" Instead of an object, a few magic strings may be used:\n" +" 'yield' - cause the current marking slice to end, as if the mark budget were\n" +" exceeded.\n" +" 'enter-weak-marking-mode' - divide the list into two segments. The items after\n" +" this string will not be marked until we enter weak marking mode. Note that weak\n" +" marking mode may be entered zero or multiple times for one GC.\n" +" 'abort-weak-marking-mode' - same as above, but then abort weak marking to fall back\n" +" on the old iterative marking code path.\n" +" 'drain' - fully drain the mark stack before continuing.\n" +" 'set-color-black' - force everything following in the mark queue to be marked black.\n" +" 'set-color-gray' - continue with the regular GC until gray marking is possible, then force\n" +" everything following in the mark queue to be marked gray.\n" +" 'unset-color' - stop forcing the mark color."), + + JS_FN_HELP("clearMarkQueue", ClearMarkQueue, 0, 0, +"clearMarkQueue()", +" Cancel the special marking of all objects enqueue with enqueueMark()."), + + JS_FN_HELP("getMarkQueue", GetMarkQueue, 0, 0, +"getMarkQueue()", +" Return the current mark queue set up via enqueueMark calls. Note that all\n" +" returned values will be wrapped into the current compartment, so this loses\n" +" some fidelity."), +#endif // DEBUG + + JS_FN_HELP("nurseryStringsEnabled", NurseryStringsEnabled, 0, 0, +"nurseryStringsEnabled()", +" Return whether strings are currently allocated in the nursery for current\n" +" global\n"), + + JS_FN_HELP("isNurseryAllocated", IsNurseryAllocated, 1, 0, +"isNurseryAllocated(thing)", +" Return whether a GC thing is nursery allocated.\n"), + + JS_FN_HELP("getLcovInfo", GetLcovInfo, 1, 0, +"getLcovInfo(global)", +" Generate LCOV tracefile for the given compartment. If no global are provided then\n" +" the current global is used as the default one.\n"), + +#ifdef DEBUG + JS_FN_HELP("setRNGState", SetRNGState, 2, 0, +"setRNGState(seed0, seed1)", +" Set this compartment's RNG state.\n"), +#endif + +#ifdef AFLFUZZ + JS_FN_HELP("aflloop", AflLoop, 1, 0, +"aflloop(max_cnt)", +" Call the __AFL_LOOP() runtime function (see AFL docs)\n"), +#endif + + JS_FN_HELP("monotonicNow", MonotonicNow, 0, 0, +"monotonicNow()", +" Return a timestamp reflecting the current elapsed system time.\n" +" This is monotonically increasing.\n"), + + JS_FN_HELP("timeSinceCreation", TimeSinceCreation, 0, 0, +"TimeSinceCreation()", +" Returns the time in milliseconds since process creation.\n" +" This uses a clock compatible with the profiler.\n"), + + JS_FN_HELP("isConstructor", IsConstructor, 1, 0, +"isConstructor(value)", +" Returns whether the value is considered IsConstructor.\n"), + + JS_FN_HELP("getTimeZone", GetTimeZone, 0, 0, +"getTimeZone()", +" Get the current time zone.\n"), + + JS_FN_HELP("getDefaultLocale", GetDefaultLocale, 0, 0, +"getDefaultLocale()", +" Get the current default locale.\n"), + + JS_FN_HELP("getCoreCount", GetCoreCount, 0, 0, +"getCoreCount()", +" Get the number of CPU cores from the platform layer. Typically this\n" +" means the number of hyperthreads on systems where that makes sense.\n"), + + JS_FN_HELP("setTimeResolution", SetTimeResolution, 2, 0, +"setTimeResolution(resolution, jitter)", +" Enables time clamping and jittering. Specify a time resolution in\n" +" microseconds and whether or not to jitter\n"), + + JS_FN_HELP("scriptedCallerGlobal", ScriptedCallerGlobal, 0, 0, +"scriptedCallerGlobal()", +" Get the caller's global (or null). See JS::GetScriptedCallerGlobal.\n"), + + JS_FN_HELP("objectGlobal", ObjectGlobal, 1, 0, +"objectGlobal(obj)", +" Returns the object's global object or null if the object is a wrapper.\n"), + + JS_FN_HELP("isSameCompartment", IsSameCompartment, 2, 0, +"isSameCompartment(obj1, obj2)", +" Unwraps obj1 and obj2 and returns whether the unwrapped objects are\n" +" same-compartment.\n"), + + JS_FN_HELP("firstGlobalInCompartment", FirstGlobalInCompartment, 1, 0, +"firstGlobalInCompartment(obj)", +" Returns the first global in obj's compartment.\n"), + + JS_FN_HELP("assertCorrectRealm", AssertCorrectRealm, 0, 0, +"assertCorrectRealm()", +" Asserts cx->realm matches callee->realm.\n"), + + JS_FN_HELP("globalLexicals", GlobalLexicals, 0, 0, +"globalLexicals()", +" Returns an object containing a copy of all global lexical bindings.\n" +" Example use: let x = 1; assertEq(globalLexicals().x, 1);\n"), + + JS_FN_HELP("baselineCompile", BaselineCompile, 2, 0, +"baselineCompile([fun/code], forceDebugInstrumentation=false)", +" Baseline-compiles the given JS function or script.\n" +" Without arguments, baseline-compiles the caller's script; but note\n" +" that extra boilerplate is needed afterwards to cause the VM to start\n" +" running the jitcode rather than staying in the interpreter:\n" +" baselineCompile(); for (var i=0; i<1; i++) {} ...\n" +" The interpreter will enter the new jitcode at the loop header unless\n" +" baselineCompile returned a string or threw an error.\n"), + + JS_FN_HELP("encodeAsUtf8InBuffer", EncodeAsUtf8InBuffer, 2, 0, +"encodeAsUtf8InBuffer(str, uint8Array)", +" Encode as many whole code points from the string str into the provided\n" +" Uint8Array as will completely fit in it, converting lone surrogates to\n" +" REPLACEMENT CHARACTER. Return an array [r, w] where |r| is the\n" +" number of 16-bit units read and |w| is the number of bytes of UTF-8\n" +" written."), + + JS_FN_HELP("clearKeptObjects", ClearKeptObjects, 0, 0, +"clearKeptObjects()", +"Perform the ECMAScript ClearKeptObjects operation, clearing the list of\n" +"observed WeakRef targets that are kept alive until the next synchronous\n" +"sequence of ECMAScript execution completes. This is used for testing\n" +"WeakRefs.\n"), + + JS_FN_HELP("numberToDouble", NumberToDouble, 1, 0, +"numberToDouble(number)", +" Return the input number as double-typed number."), + +JS_FN_HELP("getICUOptions", GetICUOptions, 0, 0, +"getICUOptions()", +" Return an object describing the following ICU options.\n\n" +" version: a string containing the ICU version number, e.g. '67.1'\n" +" unicode: a string containing the Unicode version number, e.g. '13.0'\n" +" locale: the ICU default locale, e.g. 'en_US'\n" +" tzdata: a string containing the tzdata version number, e.g. '2020a'\n" +" timezone: the ICU default time zone, e.g. 'America/Los_Angeles'\n" +" host-timezone: the host time zone, e.g. 'America/Los_Angeles'"), + +JS_FN_HELP("getAvailableLocalesOf", GetAvailableLocalesOf, 0, 0, +"getAvailableLocalesOf(name)", +" Return an array of all available locales for the given Intl constuctor."), + +JS_FN_HELP("isSmallFunction", IsSmallFunction, 1, 0, +"isSmallFunction(fun)", +" Returns true if a scripted function is small enough to be inlinable."), + + JS_FN_HELP("compileToStencil", CompileToStencil, 1, 0, +"compileToStencil(string, [options])", +" Parses the given string argument as js script, returns the stencil" +" for it."), + + JS_FN_HELP("evalStencil", EvalStencil, 1, 0, +"evalStencil(stencil, [options])", +" Instantiates the given stencil, and evaluates the top-level script it" +" defines."), + + JS_FN_HELP("compileToStencilXDR", CompileToStencilXDR, 1, 0, +"compileToStencilXDR(string, [options])", +" Parses the given string argument as js script, produces the stencil" +" for it, XDR-encodes the stencil, and returns an object that contains the" +" XDR buffer."), + + JS_FN_HELP("evalStencilXDR", EvalStencilXDR, 1, 0, +"evalStencilXDR(stencilXDR, [options])", +" Reads the given stencil XDR object, and evaluates the top-level script it" +" defines."), + + JS_FN_HELP("getExceptionInfo", GetExceptionInfo, 1, 0, +"getExceptionInfo(fun)", +" Calls the given function and returns information about the exception it" +" throws. Returns null if the function didn't throw an exception."), + + JS_FN_HELP("nukeCCW", NukeCCW, 1, 0, +"nukeCCW(wrapper)", +" Nuke a CrossCompartmentWrapper, which turns it into a DeadProxyObject."), + + JS_FS_HELP_END +}; +// clang-format on + +// clang-format off +static const JSFunctionSpecWithHelp FuzzingUnsafeTestingFunctions[] = { + JS_FN_HELP("getErrorNotes", GetErrorNotes, 1, 0, +"getErrorNotes(error)", +" Returns an array of error notes."), + + JS_FN_HELP("setTimeZone", SetTimeZone, 1, 0, +"setTimeZone(tzname)", +" Set the 'TZ' environment variable to the given time zone and applies the new time zone.\n" +" An empty string or undefined resets the time zone to its default value.\n" +" NOTE: The input string is not validated and will be passed verbatim to setenv()."), + +JS_FN_HELP("setDefaultLocale", SetDefaultLocale, 1, 0, +"setDefaultLocale(locale)", +" Set the runtime default locale to the given value.\n" +" An empty string or undefined resets the runtime locale to its default value.\n" +" NOTE: The input string is not fully validated, it must be a valid BCP-47 language tag."), + +JS_FN_HELP("isInStencilCache", IsInStencilCache, 1, 0, +"isInStencilCache(fun)", +" True if fun is available in the stencil cache."), + +JS_FN_HELP("waitForStencilCache", WaitForStencilCache, 1, 0, +"waitForStencilCache(fun)", +" Block main thread execution until the function is made available in the cache."), + +JS_FN_HELP("getInnerMostEnvironmentObject", GetInnerMostEnvironmentObject, 0, 0, +"getInnerMostEnvironmentObject()", +" Return the inner-most environment object for current execution."), + +JS_FN_HELP("getEnclosingEnvironmentObject", GetEnclosingEnvironmentObject, 1, 0, +"getEnclosingEnvironmentObject(env)", +" Return the enclosing environment object for given environment object."), + +JS_FN_HELP("getEnvironmentObjectType", GetEnvironmentObjectType, 1, 0, +"getEnvironmentObjectType(env)", +" Return a string represents the type of given environment object."), + + JS_FN_HELP("shortestPaths", ShortestPaths, 3, 0, +"shortestPaths(targets, options)", +" Return an array of arrays of shortest retaining paths. There is an array of\n" + " shortest retaining paths for each object in |targets|. Each element in a path\n" + " is of the form |{ predecessor, edge }|. |options| may contain:\n" + " \n" + " maxNumPaths: The maximum number of paths returned in each of those arrays\n" + " (default 3).\n" + " start: The object to start all paths from. If not given, then\n" + " the starting point will be the set of GC roots."), + + + JS_FS_HELP_END +}; +// clang-format on + +// clang-format off +static const JSFunctionSpecWithHelp PCCountProfilingTestingFunctions[] = { + JS_FN_HELP("start", PCCountProfiling_Start, 0, 0, + "start()", + " Start PC count profiling."), + + JS_FN_HELP("stop", PCCountProfiling_Stop, 0, 0, + "stop()", + " Stop PC count profiling."), + + JS_FN_HELP("purge", PCCountProfiling_Purge, 0, 0, + "purge()", + " Purge the collected PC count profiling data."), + + JS_FN_HELP("count", PCCountProfiling_ScriptCount, 0, 0, + "count()", + " Return the number of profiled scripts."), + + JS_FN_HELP("summary", PCCountProfiling_ScriptSummary, 1, 0, + "summary(index)", + " Return the PC count profiling summary for the given script index.\n" + " The script index must be in the range [0, pc.count())."), + + JS_FN_HELP("contents", PCCountProfiling_ScriptContents, 1, 0, + "contents(index)", + " Return the complete profiling contents for the given script index.\n" + " The script index must be in the range [0, pc.count())."), + + JS_FS_HELP_END +}; +// clang-format on + +// clang-format off +static const JSFunctionSpecWithHelp FdLibMTestingFunctions[] = { + JS_FN_HELP("pow", FdLibM_Pow, 2, 0, + "pow(x, y)", + " Return x ** y."), + + JS_FS_HELP_END +}; +// clang-format on + +bool js::InitTestingFunctions() { return disasmBuf.init(); } + +bool js::DefineTestingFunctions(JSContext* cx, HandleObject obj, + bool fuzzingSafe_, bool disableOOMFunctions_) { + fuzzingSafe = fuzzingSafe_; + if (EnvVarIsDefined("MOZ_FUZZING_SAFE")) { + fuzzingSafe = true; + } + + disableOOMFunctions = disableOOMFunctions_; + + if (!fuzzingSafe) { + if (!JS_DefineFunctionsWithHelp(cx, obj, FuzzingUnsafeTestingFunctions)) { + return false; + } + + RootedObject pccount(cx, JS_NewPlainObject(cx)); + if (!pccount) { + return false; + } + + if (!JS_DefineProperty(cx, obj, "pccount", pccount, 0)) { + return false; + } + + if (!JS_DefineFunctionsWithHelp(cx, pccount, + PCCountProfilingTestingFunctions)) { + return false; + } + } + + RootedObject fdlibm(cx, JS_NewPlainObject(cx)); + if (!fdlibm) { + return false; + } + + if (!JS_DefineProperty(cx, obj, "fdlibm", fdlibm, 0)) { + return false; + } + + if (!JS_DefineFunctionsWithHelp(cx, fdlibm, FdLibMTestingFunctions)) { + return false; + } + + return JS_DefineFunctionsWithHelp(cx, obj, TestingFunctions); +} + +#ifdef FUZZING_JS_FUZZILLI +uint32_t js::FuzzilliHashDouble(double value) { + // We shouldn't GC here as this is called directly from IC code. + AutoUnsafeCallWithABI unsafe; + uint64_t v = mozilla::BitwiseCast<uint64_t>(value); + return static_cast<uint32_t>(v) + static_cast<uint32_t>(v >> 32); +} + +uint32_t js::FuzzilliHashBigInt(BigInt* bigInt) { + // We shouldn't GC here as this is called directly from IC code. + AutoUnsafeCallWithABI unsafe; + return bigInt->hash(); +} + +void js::FuzzilliHashObject(JSContext* cx, JSObject* obj) { + // called from IC and baseline/interpreter + uint32_t hash; + FuzzilliHashObjectInl(cx, obj, &hash); + + cx->executionHashInputs += 1; + cx->executionHash = mozilla::RotateLeft(cx->executionHash + hash, 1); +} + +void js::FuzzilliHashObjectInl(JSContext* cx, JSObject* obj, uint32_t* out) { + *out = 0; + if (!js::SupportDifferentialTesting()) { + return; + } + + RootedValue v(cx); + v.setObject(*obj); + + JSAutoStructuredCloneBuffer JSCloner( + JS::StructuredCloneScope::DifferentProcess, nullptr, nullptr); + if (JSCloner.write(cx, v)) { + JSStructuredCloneData& data = JSCloner.data(); + data.ForEachDataChunk([&](const char* aData, size_t aSize) { + uint32_t h = mozilla::HashBytes(aData, aSize); + h = (h << 1) | 1; + *out ^= h; + *out *= h; + return true; + }); + } else if (JS_IsExceptionPending(cx)) { + JS_ClearPendingException(cx); + } +} +#endif diff --git a/js/src/builtin/TestingFunctions.h b/js/src/builtin/TestingFunctions.h new file mode 100644 index 0000000000..b823fabeb3 --- /dev/null +++ b/js/src/builtin/TestingFunctions.h @@ -0,0 +1,44 @@ +/* -*- 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 builtin_TestingFunctions_h +#define builtin_TestingFunctions_h + +#include "NamespaceImports.h" // JSContext, JSFunction, HandleObject, HandleValue, Value + +namespace js { + +[[nodiscard]] bool InitTestingFunctions(); + +[[nodiscard]] bool DefineTestingFunctions(JSContext* cx, HandleObject obj, + bool fuzzingSafe, + bool disableOOMFunctions); + +[[nodiscard]] bool testingFunc_assertFloat32(JSContext* cx, unsigned argc, + Value* vp); + +[[nodiscard]] bool testingFunc_assertRecoveredOnBailout(JSContext* cx, + unsigned argc, + Value* vp); + +[[nodiscard]] bool testingFunc_serialize(JSContext* cx, unsigned argc, + Value* vp); + +extern JSScript* TestingFunctionArgumentToScript(JSContext* cx, HandleValue v, + JSFunction** funp = nullptr); + +#ifdef FUZZING_JS_FUZZILLI +uint32_t FuzzilliHashDouble(double value); + +uint32_t FuzzilliHashBigInt(BigInt* bigInt); + +void FuzzilliHashObjectInl(JSContext* cx, JSObject* obj, uint32_t* out); +void FuzzilliHashObject(JSContext* cx, JSObject* obj); +#endif + +} /* namespace js */ + +#endif /* builtin_TestingFunctions_h */ diff --git a/js/src/builtin/TestingUtility.cpp b/js/src/builtin/TestingUtility.cpp new file mode 100644 index 0000000000..0e48e63480 --- /dev/null +++ b/js/src/builtin/TestingUtility.cpp @@ -0,0 +1,256 @@ +/* -*- 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 "builtin/TestingUtility.h" + +#include <stdint.h> // uint32_t + +#include "jsapi.h" // JS_NewPlainObject, JS_WrapValue +#include "js/CharacterEncoding.h" // JS_EncodeStringToUTF8 +#include "js/CompileOptions.h" // JS::CompileOptions +#include "js/Conversions.h" // JS::ToBoolean, JS::ToString, JS::ToUint32, JS::ToInt32 +#include "js/PropertyAndElement.h" // JS_GetProperty, JS_DefineProperty +#include "js/PropertyDescriptor.h" // JSPROP_ENUMERATE +#include "js/RootingAPI.h" // JS::Rooted, JS::Handle +#include "js/Utility.h" // JS::UniqueChars +#include "js/Value.h" // JS::Value, JS::StringValue +#include "vm/JSScript.h" + +bool js::ParseCompileOptions(JSContext* cx, JS::CompileOptions& options, + JS::Handle<JSObject*> opts, + JS::UniqueChars* fileNameBytes) { + JS::Rooted<JS::Value> v(cx); + JS::Rooted<JSString*> s(cx); + + if (!JS_GetProperty(cx, opts, "isRunOnce", &v)) { + return false; + } + if (!v.isUndefined()) { + options.setIsRunOnce(JS::ToBoolean(v)); + } + + if (!JS_GetProperty(cx, opts, "noScriptRval", &v)) { + return false; + } + if (!v.isUndefined()) { + options.setNoScriptRval(JS::ToBoolean(v)); + } + + if (!JS_GetProperty(cx, opts, "fileName", &v)) { + return false; + } + if (v.isNull()) { + options.setFile(nullptr); + } else if (!v.isUndefined()) { + s = JS::ToString(cx, v); + if (!s) { + return false; + } + if (fileNameBytes) { + *fileNameBytes = JS_EncodeStringToUTF8(cx, s); + if (!*fileNameBytes) { + return false; + } + options.setFile(fileNameBytes->get()); + } + } + + if (!JS_GetProperty(cx, opts, "skipFileNameValidation", &v)) { + return false; + } + if (!v.isUndefined()) { + options.setSkipFilenameValidation(JS::ToBoolean(v)); + } + + if (!JS_GetProperty(cx, opts, "lineNumber", &v)) { + return false; + } + if (!v.isUndefined()) { + uint32_t u; + if (!JS::ToUint32(cx, v, &u)) { + return false; + } + options.setLine(u); + } + + if (!JS_GetProperty(cx, opts, "columnNumber", &v)) { + return false; + } + if (!v.isUndefined()) { + int32_t c; + if (!JS::ToInt32(cx, v, &c)) { + return false; + } + options.setColumn(c); + } + + if (!JS_GetProperty(cx, opts, "sourceIsLazy", &v)) { + return false; + } + if (v.isBoolean()) { + options.setSourceIsLazy(v.toBoolean()); + } + + if (!JS_GetProperty(cx, opts, "forceFullParse", &v)) { + return false; + } + bool forceFullParseIsSet = !v.isUndefined(); + if (v.isBoolean() && v.toBoolean()) { + options.setForceFullParse(); + } + + if (!JS_GetProperty(cx, opts, "eagerDelazificationStrategy", &v)) { + return false; + } + if (forceFullParseIsSet && !v.isUndefined()) { + JS_ReportErrorASCII( + cx, "forceFullParse and eagerDelazificationStrategy are both set."); + return false; + } + if (v.isString()) { + s = JS::ToString(cx, v); + if (!s) { + return false; + } + + JSLinearString* str = JS_EnsureLinearString(cx, s); + if (!str) { + return false; + } + + bool found = false; + JS::DelazificationOption strategy = JS::DelazificationOption::OnDemandOnly; + +#define MATCH_AND_SET_STRATEGY_(NAME) \ + if (!found && JS_LinearStringEqualsLiteral(str, #NAME)) { \ + strategy = JS::DelazificationOption::NAME; \ + found = true; \ + } + + FOREACH_DELAZIFICATION_STRATEGY(MATCH_AND_SET_STRATEGY_); +#undef MATCH_AND_SET_STRATEGY_ +#undef FOR_STRATEGY_NAMES + + if (!found) { + JS_ReportErrorASCII(cx, + "eagerDelazificationStrategy does not match any " + "DelazificationOption."); + return false; + } + options.setEagerDelazificationStrategy(strategy); + } + + return true; +} + +bool js::ParseSourceOptions(JSContext* cx, JS::Handle<JSObject*> opts, + JS::MutableHandle<JSString*> displayURL, + JS::MutableHandle<JSString*> sourceMapURL) { + JS::Rooted<JS::Value> v(cx); + + if (!JS_GetProperty(cx, opts, "displayURL", &v)) { + return false; + } + if (!v.isUndefined()) { + displayURL.set(ToString(cx, v)); + if (!displayURL) { + return false; + } + } + + if (!JS_GetProperty(cx, opts, "sourceMapURL", &v)) { + return false; + } + if (!v.isUndefined()) { + sourceMapURL.set(ToString(cx, v)); + if (!sourceMapURL) { + return false; + } + } + + return true; +} + +bool js::SetSourceOptions(JSContext* cx, FrontendContext* fc, + ScriptSource* source, + JS::Handle<JSString*> displayURL, + JS::Handle<JSString*> sourceMapURL) { + if (displayURL && !source->hasDisplayURL()) { + JS::UniqueTwoByteChars chars = JS_CopyStringCharsZ(cx, displayURL); + if (!chars) { + return false; + } + if (!source->setDisplayURL(fc, std::move(chars))) { + return false; + } + } + if (sourceMapURL && !source->hasSourceMapURL()) { + JS::UniqueTwoByteChars chars = JS_CopyStringCharsZ(cx, sourceMapURL); + if (!chars) { + return false; + } + if (!source->setSourceMapURL(fc, std::move(chars))) { + return false; + } + } + + return true; +} + +JSObject* js::CreateScriptPrivate(JSContext* cx, + JS::Handle<JSString*> path /* = nullptr */) { + JS::Rooted<JSObject*> info(cx, JS_NewPlainObject(cx)); + if (!info) { + return nullptr; + } + + if (path) { + JS::Rooted<JS::Value> pathValue(cx, JS::StringValue(path)); + if (!JS_DefineProperty(cx, info, "path", pathValue, JSPROP_ENUMERATE)) { + return nullptr; + } + } + + return info; +} + +bool js::ParseDebugMetadata(JSContext* cx, JS::Handle<JSObject*> opts, + JS::MutableHandle<JS::Value> privateValue, + JS::MutableHandle<JSString*> elementAttributeName) { + JS::Rooted<JS::Value> v(cx); + JS::Rooted<JSString*> s(cx); + + if (!JS_GetProperty(cx, opts, "element", &v)) { + return false; + } + if (v.isObject()) { + JS::Rooted<JSObject*> infoObject(cx, CreateScriptPrivate(cx)); + if (!infoObject) { + return false; + } + JS::Rooted<JS::Value> elementValue(cx, v); + if (!JS_WrapValue(cx, &elementValue)) { + return false; + } + if (!JS_DefineProperty(cx, infoObject, "element", elementValue, 0)) { + return false; + } + privateValue.set(JS::ObjectValue(*infoObject)); + } + + if (!JS_GetProperty(cx, opts, "elementAttributeName", &v)) { + return false; + } + if (!v.isUndefined()) { + s = ToString(cx, v); + if (!s) { + return false; + } + elementAttributeName.set(s); + } + + return true; +} diff --git a/js/src/builtin/TestingUtility.h b/js/src/builtin/TestingUtility.h new file mode 100644 index 0000000000..8b6243e2b8 --- /dev/null +++ b/js/src/builtin/TestingUtility.h @@ -0,0 +1,64 @@ +/* -*- 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 builtin_TestingUtility_h +#define builtin_TestingUtility_h + +#include "js/RootingAPI.h" // JS::Handle, JS::MutableHandle +#include "js/Utility.h" // JS::UniqueChars + +struct JSContext; +class JSObject; +class JSString; + +namespace JS { +class JS_PUBLIC_API CompileOptions; +} + +namespace js { + +class FrontendContext; +class ScriptSource; + +// Populate `options` fields from `opt` object. +// +// `opts` can have the following properties: +// * isRunOnce (boolean): options.isRunOnce +// * noScriptRval (boolean): options.noScriptRval +// * fileName (string): options.filename_ +// enabled only when `fileNameBytes` is given, and +// `fileNameBytes` is initialized to the filename bytes +// * skipFileNameValidation (boolean): options.skipFileNameValidation_ +// * lineNumber (number): options.lineno +// * columnNumber (number): options.column +// * sourceIsLazy (boolean): options.sourceIsLazy +// * forceFullParse (boolean): options.forceFullParse_ +[[nodiscard]] bool ParseCompileOptions(JSContext* cx, + JS::CompileOptions& options, + JS::Handle<JSObject*> opts, + JS::UniqueChars* fileNameBytes); + +[[nodiscard]] bool ParseSourceOptions( + JSContext* cx, JS::Handle<JSObject*> opts, + JS::MutableHandle<JSString*> displayURL, + JS::MutableHandle<JSString*> sourceMapURL); + +[[nodiscard]] bool SetSourceOptions(JSContext* cx, FrontendContext* fc, + ScriptSource* source, + JS::Handle<JSString*> displayURL, + JS::Handle<JSString*> sourceMapURL); + +JSObject* CreateScriptPrivate(JSContext* cx, + JS::Handle<JSString*> path = nullptr); + +[[nodiscard]] bool ParseDebugMetadata( + JSContext* cx, JS::Handle<JSObject*> opts, + JS::MutableHandle<JS::Value> privateValue, + JS::MutableHandle<JSString*> elementAttributeName); + +} /* namespace js */ + +#endif /* builtin_TestingUtility_h */ diff --git a/js/src/builtin/Tuple.js b/js/src/builtin/Tuple.js new file mode 100644 index 0000000000..59a09112cd --- /dev/null +++ b/js/src/builtin/Tuple.js @@ -0,0 +1,711 @@ +/* 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/. */ + +function TupleToArray(obj) { + var len = TupleLength(obj); + var items = std_Array(len); + + for (var k = 0; k < len; k++) { + DefineDataProperty(items, k, obj[k]); + } + return items; +} + +// proposal-record-tuple +// Tuple.prototype.toSorted() +function TupleToSorted(comparefn) { + /* Step 1. */ + if (comparefn !== undefined && !IsCallable(comparefn)) { + ThrowTypeError(JSMSG_BAD_SORT_ARG); + } + + /* Step 2. */ + var T = ThisTupleValue(this); + + /* Step 3. */ + var items = TupleToArray(T); + var sorted = callFunction(ArraySort, items, comparefn); + return std_Tuple_unchecked(sorted); +} + +// proposal-record-tuple +// Tuple.prototype.toSpliced() +function TupleToSpliced(start, deleteCount /*, ...items */) { + /* Steps 1-2. */ + var list = ThisTupleValue(this); + + /* Step 3. */ + var len = TupleLength(list); + + /* Step 4. */ + var relativeStart = ToInteger(start); + + /* Step 5. */ + var actualStart; + if (relativeStart < 0) { + actualStart = std_Math_max(len + relativeStart, 0); + } else { + actualStart = std_Math_min(relativeStart, len); + } + + /* Step 6. */ + var insertCount; + var actualDeleteCount; + if (ArgumentsLength() === 0) { + insertCount = 0; + actualDeleteCount = 0; + } else if (ArgumentsLength() === 1) { + /* Step 7. */ + insertCount = 0; + actualDeleteCount = len - actualStart; + } else { + /* Step 8a. */ + insertCount = ArgumentsLength() - 2; + /* Step 8b. */ + let dc = ToInteger(deleteCount); + /* Step 8c. */ + actualDeleteCount = std_Math_min(std_Math_max(dc, 0), len - actualStart); + } + + /* Step 9. */ + if (len + insertCount - actualDeleteCount > MAX_NUMERIC_INDEX) { + ThrowTypeError(JSMSG_TOO_LONG_ARRAY); + } + + /* Step 10. */ + var k = 0; + /* Step 11. */ + /* Step 12. */ + var itemCount = insertCount; + + /* Step 13. */ + var newList = []; + + /* Step 14. */ + while (k < actualStart) { + /* Step 14a. */ + let E = list[k]; + /* Step 14b. */ + DefineDataProperty(newList, k, E); + /* Step 14c. */ + k++; + } + + /* Step 15. */ + var itemK = 0; + /* Step 16. */ + while (itemK < itemCount) { + /* Step 16a. */ + let E = GetArgument(itemK + 2); + /* Step 16b. */ + if (IsObject(E)) { + ThrowTypeError(JSMSG_RECORD_TUPLE_NO_OBJECT); + } + /* Step 16c. */ + DefineDataProperty(newList, k, E); + /* Step 16d. */ + k++; + itemK++; + } + + /* Step 17. */ + itemK = actualStart + actualDeleteCount; + /* Step 18. */ + while (itemK < len) { + /* Step 18a. */ + let E = list[itemK]; + /* Step 18b. */ + DefineDataProperty(newList, k, E); + /* Step 18c. */ + k++; + itemK++; + } + + /* Step 19. */ + return std_Tuple_unchecked(newList); +} + +// proposal-record-tuple +// Tuple.prototype.toReversed() +function TupleToReversed() { + /* Step 1. */ + var T = ThisTupleValue(this); + + /* Step 2 not necessary */ + + /* Step 3. */ + var len = TupleLength(T); + var newList = []; + + /* Step 4. */ + for (var k = len - 1; k >= 0; k--) { + /* Step 5a. */ + let E = T[k]; + /* Step 5b. */ + DefineDataProperty(newList, len - k - 1, E); + } + + /* Step 5. */ + return std_Tuple_unchecked(newList); +} + +// ES 2017 draft (April 8, 2016) 22.1.3.1.1 +function IsConcatSpreadable(O) { + // Step 1. + if (!IsObject(O) && !IsTuple(O)) { + return false; + } + + // Step 2. + var spreadable = O[GetBuiltinSymbol("isConcatSpreadable")]; + + // Step 3. + if (spreadable !== undefined) { + return ToBoolean(spreadable); + } + + if (IsTuple(O)) { + return true; + } + + // Step 4. + return IsArray(O); +} + +// proposal-record-tuple +// Tuple.prototype.concat() +function TupleConcat() { + /* Step 1 */ + var T = ThisTupleValue(this); + /* Step 2 (changed to include all elements from T). */ + var list = TupleToArray(T); + /* Step 3 */ + var n = list.length; + /* Step 4 not necessary due to changed step 2. */ + /* Step 5 */ + for (var i = 0; i < ArgumentsLength(); i++) { + /* Step 5a. */ + let E = GetArgument(i); + /* Step 5b. */ + var spreadable = IsConcatSpreadable(E); + /* Step 5c. */ + if (spreadable) { + /* Step 5c.i. */ + var k = 0; + /* Step 5c.ii */ + var len = ToLength(E.length); + /* Step 5c.iii */ + if (n + len > MAX_NUMERIC_INDEX) { + ThrowTypeError(JSMSG_TOO_LONG_ARRAY); + } + /* Step 5c.iv */ + while (k < len) { + /* Step 5c.iv.2 */ + var exists = E[k] !== undefined; + /* Step 5c.iv.3 */ + if (exists) { + /* Step 5c.iv.3.a */ + var subElement = E[k]; + /* Step 5c.iv.3.b */ + if (IsObject(subElement)) { + ThrowTypeError(JSMSG_RECORD_TUPLE_NO_OBJECT); + } + /* Step 5c.iv.3.c */ + DefineDataProperty(list, n, subElement); + /* Step 5c.iv.4 */ + n++; + } + /* Step 5c.iv.5 */ + k++; + } + } else { + /* Step 5d. */ + /* Step 5d.ii. */ + if (n >= MAX_NUMERIC_INDEX) { + ThrowTypeError(JSMSG_TOO_LONG_ARRAY); + } + /* Step 5d.iii. */ + if (IsObject(E)) { + ThrowTypeError(JSMSG_RECORD_TUPLE_NO_OBJECT); + } + /* Step 5d.iv. */ + DefineDataProperty(list, n, E); + /* Step 5d.v. */ + n++; + } + } + /* Step 6 */ + return std_Tuple_unchecked(list); +} + +// proposal-record-tuple +// Tuple.prototype.includes() +function TupleIncludes(valueToFind /* , fromIndex */) { + var fromIndex = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + return callFunction( + std_Array_includes, + ThisTupleValue(this), + valueToFind, + fromIndex + ); +} + +// proposal-record-tuple +// Tuple.prototype.indexOf() +function TupleIndexOf(valueToFind /* , fromIndex */) { + var fromIndex = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + return callFunction( + std_Array_indexOf, + ThisTupleValue(this), + valueToFind, + fromIndex + ); +} + +// proposal-record-tuple +// Tuple.prototype.join() +function TupleJoin(separator) { + let T = ThisTupleValue(this); + + // Step 2 + let len = TupleLength(T); + + // Steps 3-4 + var sep = ","; + if (!IsNullOrUndefined(separator)) { + let toString = IsCallable(separator.toString) + ? separator.toString + : std_Object_toString; + sep = callContentFunction(toString, separator); + } + + // Step 5 + var R = ""; + + // Step 6 + var k = 0; + + // Step 7 + while (k < len) { + // Step 7a + if (k > 0) { + R += sep; + } + // Step 7b + let element = T[k]; + // Step 7c + var next = ""; + if (!IsNullOrUndefined(element)) { + let toString = IsCallable(element.toString) + ? element.toString + : std_Object_toString; + next = callContentFunction(toString, element); + } + // Step 7d + R += next; + // Step 7e + k++; + } + // Step 8 + return R; +} + +// proposal-record-tuple +// Tuple.prototype.lastIndexOf() +function TupleLastIndexOf(valueToFind /* , fromIndex */) { + if (ArgumentsLength() < 2) { + return callFunction( + std_Array_lastIndexOf, + ThisTupleValue(this), + valueToFind + ); + } + return callFunction( + std_Array_lastIndexOf, + ThisTupleValue(this), + valueToFind, + GetArgument(1) + ); +} + +// proposal-record-tuple +// Tuple.prototype.toString() +function TupleToString() { + /* Step 1. */ + var T = ThisTupleValue(this); + /* Step 2. */ + var func = T.join; + if (!IsCallable(func)) { + return callFunction(std_Object_toString, T); + } + /* Step 4. */ + return callContentFunction(func, T); +} + +// proposal-record-tuple +// Tuple.prototype.toLocaleString() +function TupleToLocaleString(locales, options) { + var T = ThisTupleValue(this); + return callContentFunction( + ArrayToLocaleString, + TupleToArray(T), + locales, + options + ); +} + +// proposal-record-tuple +// Tuple.prototype.entries() +function TupleEntries() { + return CreateArrayIterator(this, ITEM_KIND_KEY_AND_VALUE); +} + +// proposal-record-tuple +// Tuple.prototype.keys() +function TupleKeys() { + return CreateArrayIterator(this, ITEM_KIND_KEY); +} + +// proposal-record-tuple +// Tuple.prototype.values() +function $TupleValues() { + return CreateArrayIterator(this, ITEM_KIND_VALUE); +} + +SetCanonicalName($TupleValues, "values"); + +// proposal-record-tuple +// Tuple.prototype.every() +function TupleEvery(callbackfn) { + return callContentFunction(ArrayEvery, ThisTupleValue(this), callbackfn); +} + +// proposal-record-tuple +// Tuple.prototype.filter() +function TupleFilter(callbackfn) { + /* Steps 1-2 */ + var list = ThisTupleValue(this); + + /* Step 3. */ + var len = TupleLength(list); + + /* Step 4. */ + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Tuple.prototype.filter"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + /* Step 5. */ + var newList = []; + + /* Step 6. */ + var k = 0; + var newK = 0; + + /* Step 7. */ + var T = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + while (k < len) { + /* Step 7a. */ + var kValue = list[k]; + /* Step 7b. */ + var selected = ToBoolean( + callContentFunction(callbackfn, T, kValue, k, list) + ); + /* Step 7c. */ + if (selected) { + /* Step 7c.i. */ + DefineDataProperty(newList, newK++, kValue); + } + /* Step 7d. */ + k++; + } + + /* Step 8. */ + return std_Tuple_unchecked(newList); +} + +// proposal-record-tuple +// Tuple.prototype.find() +function TupleFind(predicate) { + return callContentFunction(ArrayFind, ThisTupleValue(this), predicate); +} + +// proposal-record-tuple +// Tuple.prototype.findIndex() +function TupleFindIndex(predicate) { + return callContentFunction(ArrayFindIndex, ThisTupleValue(this), predicate); +} + +// proposal-record-tuple +// Tuple.prototype.forEach() +function TupleForEach(callbackfn) { + return callContentFunction(ArrayForEach, ThisTupleValue(this), callbackfn); +} + +// proposal-record-tuple +// Tuple.prototype.map() +function TupleMap(callbackfn) { + /* Steps 1-2. */ + var list = ThisTupleValue(this); + + /* Step 3. */ + var len = TupleLength(list); + + /* Step 4. */ + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + /* Step 5. */ + var newList = []; + + /* Steps 6-7. */ + var thisArg = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + for (var k = 0; k < len; k++) { + /* Step 7a. */ + var kValue = list[k]; + /* Step 7b. */ + var mappedValue = callContentFunction(callbackfn, thisArg, kValue, k, list); + /* Step 7c */ + if (IsObject(mappedValue)) { + ThrowTypeError(JSMSG_RECORD_TUPLE_NO_OBJECT); + } + /* Step 7d. */ + DefineDataProperty(newList, k, mappedValue); + } + + /* Step 8. */ + return std_Tuple_unchecked(newList); +} + +// proposal-record-tuple +// Tuple.prototype.reduce() +function TupleReduce(callbackfn /*, initialVal */) { + if (ArgumentsLength() < 2) { + return callContentFunction(ArrayReduce, ThisTupleValue(this), callbackfn); + } + return callContentFunction( + ArrayReduce, + ThisTupleValue(this), + callbackfn, + GetArgument(1) + ); +} + +// proposal-record-tuple +// Tuple.prototype.reduceRight() +function TupleReduceRight(callbackfn /*, initialVal*/) { + if (ArgumentsLength() < 2) { + return callContentFunction( + ArrayReduceRight, + ThisTupleValue(this), + callbackfn + ); + } + return callContentFunction( + ArrayReduceRight, + ThisTupleValue(this), + callbackfn, + GetArgument(1) + ); +} + +// proposal-record-tuple +// Tuple.prototype.some() +function TupleSome(callbackfn) { + return callContentFunction(ArraySome, ThisTupleValue(this), callbackfn); +} + +function FlattenIntoTuple(target, source, depth) { + /* Step 1. */ + assert(IsArray(target), "FlattenIntoTuple: target is not array"); + + /* Step 2. */ + assert(IsTuple(source), "FlattenIntoTuple: source is not tuple"); + + /* Step 3 not needed. */ + + /* Step 4. */ + var mapperFunction = undefined; + var thisArg = undefined; + var mapperIsPresent = ArgumentsLength() > 3; + if (mapperIsPresent) { + mapperFunction = GetArgument(3); + assert( + IsCallable(mapperFunction) && ArgumentsLength() > 4 && depth === 1, + "FlattenIntoTuple: mapper function must be callable, thisArg present, and depth === 1" + ); + thisArg = GetArgument(4); + } + + /* Step 5. */ + var sourceIndex = 0; + + /* Step 6. */ + for (var k = 0; k < source.length; k++) { + var element = source[k]; + /* Step 6a. */ + if (mapperIsPresent) { + /* Step 6a.i. */ + element = callContentFunction( + mapperFunction, + thisArg, + element, + sourceIndex, + source + ); + /* Step 6a.ii. */ + if (IsObject(element)) { + ThrowTypeError(JSMSG_RECORD_TUPLE_NO_OBJECT); + } + } + /* Step 6b. */ + if (depth > 0 && IsTuple(element)) { + FlattenIntoTuple(target, element, depth - 1); + } else { + /* Step 6c.i. */ + var len = ToLength(target.length); + /* Step 6c.ii. */ + if (len > MAX_NUMERIC_INDEX) { + ThrowTypeError(JSMSG_TOO_LONG_ARRAY); + } + /* Step 6c.iii. */ + DefineDataProperty(target, len, element); + } + /* Step 6d. */ + sourceIndex++; + } +} + +// proposal-record-tuple +// Tuple.prototype.flat() +function TupleFlat() { + /* Steps 1-2. */ + var list = ThisTupleValue(this); + + /* Step 3. */ + var depthNum = 1; + + /* Step 4. */ + if (ArgumentsLength() && GetArgument(0) !== undefined) { + depthNum = ToInteger(GetArgument(0)); + } + + /* Step 5. */ + var flat = []; + + /* Step 6. */ + FlattenIntoTuple(flat, list, depthNum); + + /* Step 7. */ + return std_Tuple_unchecked(flat); +} + +// proposal-record-tuple +// Tuple.prototype.flatMap() +function TupleFlatMap(mapperFunction /*, thisArg*/) { + /* Steps 1-2. */ + var list = ThisTupleValue(this); + + /* Step 3. */ + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "Tuple.prototype.flatMap"); + } + if (!IsCallable(mapperFunction)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, mapperFunction)); + } + + /* Step 4. */ + var flat = []; + + /* Step 5. */ + var thisArg = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + FlattenIntoTuple(flat, list, 1, mapperFunction, thisArg); + + /* Step 6. */ + return std_Tuple_unchecked(flat); +} + +function TupleFrom(items /*, mapFn, thisArg */) { + /* Step 1 */ + var mapping; + var mapfn = ArgumentsLength() < 2 ? undefined : GetArgument(1); + var thisArg = ArgumentsLength() < 3 ? undefined : GetArgument(2); + if (mapfn === undefined) { + mapping = false; + } else { + /* Step 2 */ + if (!IsCallable(mapfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, GetArgument(1))); + } + mapping = true; + } + + /* Step 3 */ + var list = []; + + /* Step 4 */ + var k = 0; + + /* Step 5 */ + let usingIterator = GetMethod(items, GetBuiltinSymbol("iterator")); + + /* Step 6 */ + if (usingIterator !== undefined) { + /* Step 6a. */ + let adder = function(value) { + var mappedValue; + /* Step 6a.i */ + if (mapping) { + /* Step 6a.i.1. */ + mappedValue = callContentFunction(mapfn, thisArg, value, k); + } else { + /* Step 6a.ii. */ + mappedValue = value; + } + /* Step 6a.iii. */ + if (IsObject(mappedValue)) { + ThrowTypeError(JSMSG_RECORD_TUPLE_NO_OBJECT); + } + /* Step 6a.iv. */ + DefineDataProperty(list, k, mappedValue); + /* Step 6a.v. */ + k++; + }; + /* Step 6b. */ + for (var nextValue of allowContentIterWith(items, usingIterator)) { + adder(nextValue); + } + /* Step 6c. */ + return std_Tuple_unchecked(list); + } + /* Step 7 */ + /* We assume items is an array-like object */ + /* Step 8 */ + let arrayLike = ToObject(items); + /* Step 9 */ + let len = ToLength(arrayLike.length); + /* Step 10 */ + while (k < len) { + /* Step 10a not necessary */ + /* Step 10b */ + let kValue = arrayLike[k]; + /* Step 10c-d */ + let mappedValue = mapping + ? callContentFunction(mapfn, thisArg, kValue, k) + : kValue; + /* Step 10e */ + if (IsObject(mappedValue)) { + ThrowTypeError(JSMSG_RECORD_TUPLE_NO_OBJECT); + } + /* Step 10f */ + DefineDataProperty(list, k, mappedValue); + /* Step 10g */ + k++; + } + /* Step 11 */ + return std_Tuple_unchecked(list); +} diff --git a/js/src/builtin/TupleObject.cpp b/js/src/builtin/TupleObject.cpp new file mode 100644 index 0000000000..524e909262 --- /dev/null +++ b/js/src/builtin/TupleObject.cpp @@ -0,0 +1,102 @@ +/* -*- 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 "builtin/TupleObject.h" + +#include "mozilla/Assertions.h" + +#include "jsapi.h" + +#include "vm/NativeObject.h" +#include "vm/ObjectOperations.h" +#include "vm/TupleType.h" + +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +// Record and Tuple proposal section 9.2.1 + +TupleObject* TupleObject::create(JSContext* cx, Handle<TupleType*> tuple) { + TupleObject* tup = NewBuiltinClassInstance<TupleObject>(cx); + if (!tup) { + return nullptr; + } + tup->setFixedSlot(PrimitiveValueSlot, ExtendedPrimitiveValue(*tuple)); + return tup; +} + +// Caller is responsible for rooting the result +TupleType& TupleObject::unbox() const { + return getFixedSlot(PrimitiveValueSlot).toExtendedPrimitive().as<TupleType>(); +} + +// Caller is responsible for rooting the result +mozilla::Maybe<TupleType&> TupleObject::maybeUnbox(JSObject* obj) { + Maybe<TupleType&> result = mozilla::Nothing(); + if (obj->is<TupleType>()) { + result.emplace(obj->as<TupleType>()); + } else if (obj->is<TupleObject>()) { + result.emplace(obj->as<TupleObject>().unbox()); + } + return result; +} + +bool js::IsTuple(JSObject& obj) { + return (obj.is<TupleType>() || obj.is<TupleObject>()); +} + +// Caller is responsible for rooting the result +mozilla::Maybe<TupleType&> js::ThisTupleValue(JSContext* cx, HandleValue val) { + if (!js::IsTuple(val)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_TUPLE_OBJECT); + return mozilla::Nothing(); + } + Maybe<TupleType&> result = mozilla::Nothing(); + result.emplace(TupleType::thisTupleValue(val)); + return (result); +} + +bool tup_mayResolve(const JSAtomState&, jsid id, JSObject*) { + // tup_resolve ignores non-integer ids. + return id.isInt(); +} + +bool tup_resolve(JSContext* cx, HandleObject obj, HandleId id, + bool* resolvedp) { + RootedValue value(cx); + *resolvedp = obj->as<TupleObject>().unbox().getOwnProperty(id, &value); + + if (*resolvedp) { + static const unsigned TUPLE_ELEMENT_ATTRS = + JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT; + return DefineDataProperty(cx, obj, id, value, + TUPLE_ELEMENT_ATTRS | JSPROP_RESOLVING); + } + + return true; +} + +const JSClassOps TupleObjectClassOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + tup_resolve, // resolve + tup_mayResolve, // mayResolve + nullptr, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass TupleObject::class_ = { + "TupleObject", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | + JSCLASS_HAS_CACHED_PROTO(JSProto_Tuple), + &TupleObjectClassOps}; diff --git a/js/src/builtin/TupleObject.h b/js/src/builtin/TupleObject.h new file mode 100644 index 0000000000..54875b8ba9 --- /dev/null +++ b/js/src/builtin/TupleObject.h @@ -0,0 +1,34 @@ +/* -*- 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 builtin_TupleObject_h +#define builtin_TupleObject_h + +#include "vm/NativeObject.h" +#include "vm/TupleType.h" + +namespace js { + +[[nodiscard]] mozilla::Maybe<TupleType&> ThisTupleValue(JSContext* cx, + HandleValue val); + +class TupleObject : public NativeObject { + enum { PrimitiveValueSlot, SlotCount }; + + public: + static const JSClass class_; + + static TupleObject* create(JSContext* cx, Handle<TupleType*> tuple); + + JS::TupleType& unbox() const; + + static mozilla::Maybe<TupleType&> maybeUnbox(JSObject* obj); +}; + +bool IsTuple(JSObject& obj); +} // namespace js + +#endif diff --git a/js/src/builtin/TypedArray.js b/js/src/builtin/TypedArray.js new file mode 100644 index 0000000000..6f0e713abf --- /dev/null +++ b/js/src/builtin/TypedArray.js @@ -0,0 +1,2268 @@ +/* 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 "TypedArrayConstants.h" + +function ViewedArrayBufferIfReified(tarray) { + assert(IsTypedArray(tarray), "non-typed array asked for its buffer"); + + var buf = UnsafeGetReservedSlot(tarray, JS_TYPEDARRAYLAYOUT_BUFFER_SLOT); + assert( + buf === null || + (IsObject(buf) && + (GuardToArrayBuffer(buf) !== null || + GuardToSharedArrayBuffer(buf) !== null)), + "unexpected value in buffer slot" + ); + return buf; +} + +function IsDetachedBuffer(buffer) { + // A typed array with a null buffer has never had its buffer exposed to + // become detached. + if (buffer === null) { + return false; + } + + assert( + GuardToArrayBuffer(buffer) !== null || + GuardToSharedArrayBuffer(buffer) !== null, + "non-ArrayBuffer passed to IsDetachedBuffer" + ); + + // Shared array buffers are not detachable. + // + // This check is more expensive than desirable, but IsDetachedBuffer is + // only hot for non-shared memory in SetFromNonTypedArray, so there is an + // optimization in place there to avoid incurring the cost here. An + // alternative is to give SharedArrayBuffer the same layout as ArrayBuffer. + if ((buffer = GuardToArrayBuffer(buffer)) === null) { + return false; + } + + var flags = UnsafeGetInt32FromReservedSlot(buffer, JS_ARRAYBUFFER_FLAGS_SLOT); + return (flags & JS_ARRAYBUFFER_DETACHED_FLAG) !== 0; +} + +function TypedArrayLengthMethod() { + return TypedArrayLength(this); +} + +function GetAttachedArrayBuffer(tarray) { + var buffer = ViewedArrayBufferIfReified(tarray); + if (IsDetachedBuffer(buffer)) { + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + } + return buffer; +} + +function GetAttachedArrayBufferMethod() { + return GetAttachedArrayBuffer(this); +} + +// A function which ensures that the argument is either a typed array or a +// cross-compartment wrapper for a typed array and that the typed array involved +// has an attached array buffer. If one of those conditions doesn't hold (wrong +// kind of argument, or detached array buffer), an exception is thrown. The +// return value is `true` if the argument is a typed array, `false` if it's a +// cross-compartment wrapper for a typed array. +function IsTypedArrayEnsuringArrayBuffer(arg) { + if (IsObject(arg) && IsTypedArray(arg)) { + GetAttachedArrayBuffer(arg); + return true; + } + + callFunction( + CallTypedArrayMethodIfWrapped, + arg, + "GetAttachedArrayBufferMethod" + ); + return false; +} + +// ES2019 draft rev 85ce767c86a1a8ed719fe97e978028bff819d1f2 +// 7.3.20 SpeciesConstructor ( O, defaultConstructor ) +// +// SpeciesConstructor function optimized for TypedArrays to avoid calling +// ConstructorForTypedArray, a non-inlineable runtime function, in the normal +// case. +function TypedArraySpeciesConstructor(obj) { + // Step 1. + assert(IsObject(obj), "not passed an object"); + + // Step 2. + var ctor = obj.constructor; + + // Step 3. + if (ctor === undefined) { + return ConstructorForTypedArray(obj); + } + + // Step 4. + if (!IsObject(ctor)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, "object's 'constructor' property"); + } + + // Steps 5. + var s = ctor[GetBuiltinSymbol("species")]; + + // Step 6. + if (IsNullOrUndefined(s)) { + return ConstructorForTypedArray(obj); + } + + // Step 7. + if (IsConstructor(s)) { + return s; + } + + // Step 8. + ThrowTypeError( + JSMSG_NOT_CONSTRUCTOR, + "@@species property of object's constructor" + ); +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 22.2.3.5.1 Runtime Semantics: ValidateTypedArray ( O ) +function ValidateTypedArray(obj) { + if (IsObject(obj)) { + /* Steps 3-5 (non-wrapped typed arrays). */ + if (IsTypedArray(obj)) { + // GetAttachedArrayBuffer throws for detached array buffers. + GetAttachedArrayBuffer(obj); + return true; + } + + /* Steps 3-5 (wrapped typed arrays). */ + if (IsPossiblyWrappedTypedArray(obj)) { + if (PossiblyWrappedTypedArrayHasDetachedBuffer(obj)) { + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + } + return false; + } + } + + /* Steps 1-2. */ + ThrowTypeError(JSMSG_NON_TYPED_ARRAY_RETURNED); +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 22.2.4.6 TypedArrayCreate ( constructor, argumentList ) +function TypedArrayCreateWithLength(constructor, length) { + // Step 1. + var newTypedArray = constructContentFunction( + constructor, + constructor, + length + ); + + // Step 2. + var isTypedArray = ValidateTypedArray(newTypedArray); + + // Step 3. + var len; + if (isTypedArray) { + len = TypedArrayLength(newTypedArray); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + newTypedArray, + "TypedArrayLengthMethod" + ); + } + + if (len < length) { + ThrowTypeError(JSMSG_SHORT_TYPED_ARRAY_RETURNED, length, len); + } + + // Step 4. + return newTypedArray; +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 22.2.4.6 TypedArrayCreate ( constructor, argumentList ) +function TypedArrayCreateWithBuffer(constructor, buffer, byteOffset, length) { + // Step 1. + var newTypedArray = constructContentFunction( + constructor, + constructor, + buffer, + byteOffset, + length + ); + + // Step 2. + ValidateTypedArray(newTypedArray); + + // Step 3 (not applicable). + + // Step 4. + return newTypedArray; +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 22.2.4.7 TypedArraySpeciesCreate ( exemplar, argumentList ) +function TypedArraySpeciesCreateWithLength(exemplar, length) { + // Step 1 (omitted). + + // Steps 2-3. + var C = TypedArraySpeciesConstructor(exemplar); + + // Step 4. + return TypedArrayCreateWithLength(C, length); +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 22.2.4.7 TypedArraySpeciesCreate ( exemplar, argumentList ) +function TypedArraySpeciesCreateWithBuffer( + exemplar, + buffer, + byteOffset, + length +) { + // Step 1 (omitted). + + // Steps 2-3. + var C = TypedArraySpeciesConstructor(exemplar); + + // Step 4. + return TypedArrayCreateWithBuffer(C, buffer, byteOffset, length); +} + +// ES6 draft rev30 (2014/12/24) 22.2.3.6 %TypedArray%.prototype.entries() +function TypedArrayEntries() { + // Step 1. + var O = this; + + // We need to be a bit careful here, because in the Xray case we want to + // create the iterator in our current compartment. + // + // Before doing that, though, we want to check that we have a typed array + // and it does not have a detached array buffer. We do the latter by just + // calling GetAttachedArrayBuffer() and letting it throw if there isn't one. + // In the case when we're not sure we have a typed array (e.g. we might have + // a cross-compartment wrapper for one), we can go ahead and call + // GetAttachedArrayBuffer via IsTypedArrayEnsuringArrayBuffer; that will + // throw if we're not actually a wrapped typed array, or if we have a + // detached array buffer. + + // Step 2-6. + IsTypedArrayEnsuringArrayBuffer(O); + + // Step 7. + return CreateArrayIterator(O, ITEM_KIND_KEY_AND_VALUE); +} + +// ES2021 draft rev 190d474c3d8728653fbf8a5a37db1de34b9c1472 +// Plus <https://github.com/tc39/ecma262/pull/2221> +// 22.2.3.7 %TypedArray%.prototype.every ( callbackfn [ , thisArg ] ) +function TypedArrayEvery(callbackfn /*, thisArg*/) { + // Step 1. + var O = this; + + // Step 2. + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(O); + + // If we got here, `this` is either a typed array or a wrapper for one. + + // Step 3. + var len; + if (isTypedArray) { + len = TypedArrayLength(O); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + O, + "TypedArrayLengthMethod" + ); + } + + // Step 4. + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "%TypedArray%.prototype.every"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + var thisArg = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Steps 5-6. + for (var k = 0; k < len; k++) { + // Steps 6.b-d. + var kValue = O[k]; + + // Step 6.c. + var testResult = callContentFunction(callbackfn, thisArg, kValue, k, O); + + // Step 6.d. + if (!testResult) { + return false; + } + } + + // Step 7. + return true; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(TypedArrayEvery); + +// ES2018 draft rev ad2d1c60c5dc42a806696d4b58b4dca42d1f7dd4 +// 22.2.3.8 %TypedArray%.prototype.fill ( value [ , start [ , end ] ] ) +function TypedArrayFill(value, start = 0, end = undefined) { + // This function is not generic. + if (!IsObject(this) || !IsTypedArray(this)) { + return callFunction( + CallTypedArrayMethodIfWrapped, + this, + value, + start, + end, + "TypedArrayFill" + ); + } + + // Step 1. + var O = this; + + // Step 2. + var buffer = GetAttachedArrayBuffer(this); + + // Step 3. + var len = TypedArrayLength(O); + + // Step 4. + var kind = GetTypedArrayKind(O); + if (kind === TYPEDARRAY_KIND_BIGINT64 || kind === TYPEDARRAY_KIND_BIGUINT64) { + value = ToBigInt(value); + } else { + value = ToNumber(value); + } + + // Step 5. + var relativeStart = ToInteger(start); + + // Step 6. + var k = + relativeStart < 0 + ? std_Math_max(len + relativeStart, 0) + : std_Math_min(relativeStart, len); + + // Step 7. + var relativeEnd = end === undefined ? len : ToInteger(end); + + // Step 8. + var final = + relativeEnd < 0 + ? std_Math_max(len + relativeEnd, 0) + : std_Math_min(relativeEnd, len); + + // Step 9. + if (buffer === null) { + // A typed array previously using inline storage may acquire a + // buffer, so we must check with the source. + buffer = ViewedArrayBufferIfReified(O); + } + + if (IsDetachedBuffer(buffer)) { + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + } + + // Step 10. + for (; k < final; k++) { + O[k] = value; + } + + // Step 11. + return O; +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// %TypedArray%.prototype.filter ( callbackfn [ , thisArg ] ) +function TypedArrayFilter(callbackfn /*, thisArg*/) { + // Step 1. + var O = this; + + // Step 2. + // This function is not generic. + // We want to make sure that we have an attached buffer, per spec prose. + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(O); + + // If we got here, `this` is either a typed array or a wrapper for one. + + // Step 3. + var len; + if (isTypedArray) { + len = TypedArrayLength(O); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + O, + "TypedArrayLengthMethod" + ); + } + + // Step 4. + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "%TypedArray%.prototype.filter"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + // Step 5. + var T = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 6. + var kept = new_List(); + + // Step 8. + var captured = 0; + + // Steps 7 and 9.e. + for (var k = 0; k < len; k++) { + // Steps 9.a-b. + var kValue = O[k]; + + // Steps 9.c-d. + if (callContentFunction(callbackfn, T, kValue, k, O)) { + // Steps 9.d.i-ii. + kept[captured++] = kValue; + } + } + + // Step 10. + var A = TypedArraySpeciesCreateWithLength(O, captured); + + // Steps 11 and 12.b. + for (var n = 0; n < captured; n++) { + // Step 12.a. + A[n] = kept[n]; + } + + // Step 13. + return A; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(TypedArrayFilter); + +// ES2021 draft rev 190d474c3d8728653fbf8a5a37db1de34b9c1472 +// Plus <https://github.com/tc39/ecma262/pull/2221> +// 22.2.3.10 %TypedArray%.prototype.find ( predicate [ , thisArg ] ) +function TypedArrayFind(predicate /*, thisArg*/) { + // Step 1. + var O = this; + + // Step 2. + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(O); + + // If we got here, `this` is either a typed array or a wrapper for one. + + // Step 3. + var len; + if (isTypedArray) { + len = TypedArrayLength(O); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + O, + "TypedArrayLengthMethod" + ); + } + + // Step 4. + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "%TypedArray%.prototype.find"); + } + if (!IsCallable(predicate)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, predicate)); + } + + var thisArg = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Steps 5-6. + for (var k = 0; k < len; k++) { + // Steps 6.a-b. + var kValue = O[k]; + + // Steps 6.c-d. + if (callContentFunction(predicate, thisArg, kValue, k, O)) { + return kValue; + } + } + + // Step 7. + return undefined; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(TypedArrayFind); + +// ES2021 draft rev 190d474c3d8728653fbf8a5a37db1de34b9c1472 +// Plus <https://github.com/tc39/ecma262/pull/2221> +// 22.2.3.11 %TypedArray%.prototype.findIndex ( predicate [ , thisArg ] ) +function TypedArrayFindIndex(predicate /*, thisArg*/) { + // Step 1. + var O = this; + + // Step 2. + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(O); + + // If we got here, `this` is either a typed array or a wrapper for one. + + // Step 3. + var len; + if (isTypedArray) { + len = TypedArrayLength(O); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + O, + "TypedArrayLengthMethod" + ); + } + + // Step 4. + if (ArgumentsLength() === 0) { + ThrowTypeError( + JSMSG_MISSING_FUN_ARG, + 0, + "%TypedArray%.prototype.findIndex" + ); + } + if (!IsCallable(predicate)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, predicate)); + } + + var thisArg = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Steps 5-6. + for (var k = 0; k < len; k++) { + // Steps 6.a-f. + if (callContentFunction(predicate, thisArg, O[k], k, O)) { + return k; + } + } + + // Step 7. + return -1; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(TypedArrayFindIndex); + +// ES2021 draft rev 190d474c3d8728653fbf8a5a37db1de34b9c1472 +// Plus <https://github.com/tc39/ecma262/pull/2221> +// 22.2.3.12 %TypedArray%.prototype.forEach ( callbackfn [ , thisArg ] ) +function TypedArrayForEach(callbackfn /*, thisArg*/) { + // Step 1. + var O = this; + + // Step 2. + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(O); + + // If we got here, `this` is either a typed array or a wrapper for one. + + // Step 3. + var len; + if (isTypedArray) { + len = TypedArrayLength(O); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + O, + "TypedArrayLengthMethod" + ); + } + + // Step 4. + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "TypedArray.prototype.forEach"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + var thisArg = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Steps 5-6. + for (var k = 0; k < len; k++) { + // Steps 6.a-c. + callContentFunction(callbackfn, thisArg, O[k], k, O); + } + + // Step 7. + return undefined; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(TypedArrayForEach); + +// ES2021 draft rev 190d474c3d8728653fbf8a5a37db1de34b9c1472 +// Plus <https://github.com/tc39/ecma262/pull/2221> +// 22.2.3.14 %TypedArray%.prototype.indexOf ( searchElement [ , fromIndex ] ) +function TypedArrayIndexOf(searchElement, fromIndex = 0) { + // Step 2. + if (!IsObject(this) || !IsTypedArray(this)) { + return callFunction( + CallTypedArrayMethodIfWrapped, + this, + searchElement, + fromIndex, + "TypedArrayIndexOf" + ); + } + + GetAttachedArrayBuffer(this); + + // Step 1. + var O = this; + + // Step 3. + var len = TypedArrayLength(O); + + // Step 4. + if (len === 0) { + return -1; + } + + // Step 5. + var n = ToInteger(fromIndex); + + // Step 6. + assert(fromIndex !== undefined || n === 0, "ToInteger(undefined) is zero"); + + // Reload O.[[ArrayLength]] in case ToInteger() detached the ArrayBuffer. + // This let's us avoid executing the HasProperty operation in step 11.a. + len = TypedArrayLength(O); + + assert( + len === 0 || !IsDetachedBuffer(ViewedArrayBufferIfReified(O)), + "TypedArrays with detached buffers have a length of zero" + ); + + // Step 7. + if (n >= len) { + return -1; + } + + // Steps 7-10. + // Steps 7-8 are handled implicitly. + var k; + if (n >= 0) { + // Step 9.a. + k = n; + } else { + // Step 10.a. + k = len + n; + + // Step 10.b. + if (k < 0) { + k = 0; + } + } + + // Step 11. + for (; k < len; k++) { + // Step 11.a (not necessary in our implementation). + assert(k in O, "unexpected missing element"); + + // Steps 11.b.i-iii. + if (O[k] === searchElement) { + return k; + } + } + + // Step 12. + return -1; +} + +// ES2021 draft rev 190d474c3d8728653fbf8a5a37db1de34b9c1472 +// Plus <https://github.com/tc39/ecma262/pull/2221> +// 22.2.3.15 %TypedArray%.prototype.join ( separator ) +function TypedArrayJoin(separator) { + // Step 2. + if (!IsObject(this) || !IsTypedArray(this)) { + return callFunction( + CallTypedArrayMethodIfWrapped, + this, + separator, + "TypedArrayJoin" + ); + } + + GetAttachedArrayBuffer(this); + + // Step 1. + var O = this; + + // Step 3. + var len = TypedArrayLength(O); + + // Steps 4-5. + var sep = separator === undefined ? "," : ToString(separator); + + // Steps 6 and 9. + if (len === 0) { + return ""; + } + + // ToString() might have detached the underlying ArrayBuffer. To avoid + // checking for this condition when looping in step 8.c, do it once here. + if (TypedArrayLength(O) === 0) { + assert( + IsDetachedBuffer(ViewedArrayBufferIfReified(O)), + "TypedArrays with detached buffers have a length of zero" + ); + + return callFunction(String_repeat, ",", len - 1); + } + + assert( + !IsDetachedBuffer(ViewedArrayBufferIfReified(O)), + "TypedArrays with detached buffers have a length of zero" + ); + + var element0 = O[0]; + + // Omit the 'if' clause in step 8.c, since typed arrays can't have undefined or null elements. + assert(element0 !== undefined, "unexpected undefined element"); + + // Step 6. + var R = ToString(element0); + + // Steps 7-8. + for (var k = 1; k < len; k++) { + // Step 8.b. + var element = O[k]; + + // Omit the 'if' clause in step 8.c, since typed arrays can't have undefined or null elements. + assert(element !== undefined, "unexpected undefined element"); + + // Steps 8.a and 8.c-d. + R += sep + ToString(element); + } + + // Step 9. + return R; +} + +// ES6 draft (2016/1/11) 22.2.3.15 %TypedArray%.prototype.keys() +function TypedArrayKeys() { + // Step 1. + var O = this; + + // See the big comment in TypedArrayEntries for what we're doing here. + + // Step 2. + IsTypedArrayEnsuringArrayBuffer(O); + + // Step 3. + return CreateArrayIterator(O, ITEM_KIND_KEY); +} + +// ES2021 draft rev 190d474c3d8728653fbf8a5a37db1de34b9c1472 +// Plus <https://github.com/tc39/ecma262/pull/2221> +// 22.2.3.17 %TypedArray%.prototype.lastIndexOf ( searchElement [ , fromIndex ] ) +function TypedArrayLastIndexOf(searchElement /*, fromIndex*/) { + // Step 2. + if (!IsObject(this) || !IsTypedArray(this)) { + if (ArgumentsLength() > 1) { + return callFunction( + CallTypedArrayMethodIfWrapped, + this, + searchElement, + GetArgument(1), + "TypedArrayLastIndexOf" + ); + } + return callFunction( + CallTypedArrayMethodIfWrapped, + this, + searchElement, + "TypedArrayLastIndexOf" + ); + } + + GetAttachedArrayBuffer(this); + + // Step 1. + var O = this; + + // Step 3. + var len = TypedArrayLength(O); + + // Step 4. + if (len === 0) { + return -1; + } + + // Step 5. + var n = ArgumentsLength() > 1 ? ToInteger(GetArgument(1)) : len - 1; + + // Reload O.[[ArrayLength]] in case ToInteger() detached the ArrayBuffer. + // This let's us avoid executing the HasProperty operation in step 9.a. + len = TypedArrayLength(O); + + assert( + len === 0 || !IsDetachedBuffer(ViewedArrayBufferIfReified(O)), + "TypedArrays with detached buffers have a length of zero" + ); + + // Steps 6-8. + var k = n >= 0 ? std_Math_min(n, len - 1) : len + n; + + // Step 9. + for (; k >= 0; k--) { + // Step 9.a (not necessary in our implementation). + assert(k in O, "unexpected missing element"); + + // Steps 9.b.i-iii. + if (O[k] === searchElement) { + return k; + } + } + + // Step 10. + return -1; +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 22.2.3.19 %TypedArray%.prototype.map ( callbackfn [ , thisArg ] ) +function TypedArrayMap(callbackfn /*, thisArg*/) { + // Step 1. + var O = this; + + // Step 2. + // This function is not generic. + // We want to make sure that we have an attached buffer, per spec prose. + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(O); + + // If we got here, `this` is either a typed array or a wrapper for one. + + // Step 3. + var len; + if (isTypedArray) { + len = TypedArrayLength(O); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + O, + "TypedArrayLengthMethod" + ); + } + + // Step 4. + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "%TypedArray%.prototype.map"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + // Step 5. + var T = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 6. + var A = TypedArraySpeciesCreateWithLength(O, len); + + // Steps 7, 8.a (implicit) and 8.e. + for (var k = 0; k < len; k++) { + // Steps 8.b-c. + var mappedValue = callContentFunction(callbackfn, T, O[k], k, O); + + // Steps 8.d. + A[k] = mappedValue; + } + + // Step 9. + return A; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(TypedArrayMap); + +// ES2021 draft rev 190d474c3d8728653fbf8a5a37db1de34b9c1472 +// Plus <https://github.com/tc39/ecma262/pull/2221> +// 22.2.3.20 %TypedArray%.prototype.reduce ( callbackfn [ , initialValue ] ) +function TypedArrayReduce(callbackfn /*, initialValue*/) { + // Step 1. + var O = this; + + // Step 2. + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(O); + + // If we got here, `this` is either a typed array or a wrapper for one. + + // Step 3. + var len; + if (isTypedArray) { + len = TypedArrayLength(O); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + O, + "TypedArrayLengthMethod" + ); + } + + // Step 4. + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "%TypedArray%.prototype.reduce"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + // Step 5. + if (len === 0 && ArgumentsLength() === 1) { + ThrowTypeError(JSMSG_EMPTY_ARRAY_REDUCE); + } + + // Step 6. + var k = 0; + + // Steps 7-9. + var accumulator = ArgumentsLength() > 1 ? GetArgument(1) : O[k++]; + + // Step 10. + for (; k < len; k++) { + accumulator = callContentFunction( + callbackfn, + undefined, + accumulator, + O[k], + k, + O + ); + } + + // Step 11. + return accumulator; +} + +// ES2021 draft rev 190d474c3d8728653fbf8a5a37db1de34b9c1472 +// Plus <https://github.com/tc39/ecma262/pull/2221> +// 22.2.3.21 %TypedArray%.prototype.reduceRight ( callbackfn [ , initialValue ] ) +function TypedArrayReduceRight(callbackfn /*, initialValue*/) { + // Step 1. + var O = this; + + // Step 2. + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(O); + + // If we got here, `this` is either a typed array or a wrapper for one. + + // Step 3. + var len; + if (isTypedArray) { + len = TypedArrayLength(O); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + O, + "TypedArrayLengthMethod" + ); + } + + // Step 4. + if (ArgumentsLength() === 0) { + ThrowTypeError( + JSMSG_MISSING_FUN_ARG, + 0, + "%TypedArray%.prototype.reduceRight" + ); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + // Step 5. + if (len === 0 && ArgumentsLength() === 1) { + ThrowTypeError(JSMSG_EMPTY_ARRAY_REDUCE); + } + + // Step 6. + var k = len - 1; + + // Steps 7-9. + var accumulator = ArgumentsLength() > 1 ? GetArgument(1) : O[k--]; + + // Step 10. + for (; k >= 0; k--) { + accumulator = callContentFunction( + callbackfn, + undefined, + accumulator, + O[k], + k, + O + ); + } + + // Step 11. + return accumulator; +} + +// ES2021 draft rev 190d474c3d8728653fbf8a5a37db1de34b9c1472 +// Plus <https://github.com/tc39/ecma262/pull/2221> +// 22.2.3.22 %TypedArray%.prototype.reverse ( ) +function TypedArrayReverse() { + // Step 2. + if (!IsObject(this) || !IsTypedArray(this)) { + return callFunction( + CallTypedArrayMethodIfWrapped, + this, + "TypedArrayReverse" + ); + } + + GetAttachedArrayBuffer(this); + + // Step 1. + var O = this; + + // Step 3. + var len = TypedArrayLength(O); + + // Step 4. + var middle = std_Math_floor(len / 2); + + // Steps 5-6. + for (var lower = 0; lower !== middle; lower++) { + // Step 6.a. + var upper = len - lower - 1; + + // Step 6.d. + var lowerValue = O[lower]; + + // Step 6.e. + var upperValue = O[upper]; + + // Steps 6.f-g. + O[lower] = upperValue; + O[upper] = lowerValue; + } + + // Step 7. + return O; +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 22.2.3.24 %TypedArray%.prototype.slice ( start, end ) +function TypedArraySlice(start, end) { + // Step 1. + var O = this; + + // Step 2. + if (!IsObject(O) || !IsTypedArray(O)) { + return callFunction( + CallTypedArrayMethodIfWrapped, + O, + start, + end, + "TypedArraySlice" + ); + } + + var buffer = GetAttachedArrayBuffer(O); + + // Step 3. + var len = TypedArrayLength(O); + + // Step 4. + var relativeStart = ToInteger(start); + + // Step 5. + var k = + relativeStart < 0 + ? std_Math_max(len + relativeStart, 0) + : std_Math_min(relativeStart, len); + + // Step 6. + var relativeEnd = end === undefined ? len : ToInteger(end); + + // Step 7. + var final = + relativeEnd < 0 + ? std_Math_max(len + relativeEnd, 0) + : std_Math_min(relativeEnd, len); + + // Step 8. + var count = std_Math_max(final - k, 0); + + // Step 9. + var A = TypedArraySpeciesCreateWithLength(O, count); + + // Steps 14-15. + if (count > 0) { + // Steps 14.b.ii, 15.b. + if (buffer === null) { + // A typed array previously using inline storage may acquire a + // buffer, so we must check with the source. + buffer = ViewedArrayBufferIfReified(O); + } + + if (IsDetachedBuffer(buffer)) { + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + } + + // Steps 10-13, 15. + var sliced = TypedArrayBitwiseSlice(O, A, k, count); + + // Step 14. + if (!sliced) { + // Step 14.a. + var n = 0; + + // Step 14.b. + while (k < final) { + // Steps 14.b.i-v. + A[n++] = O[k++]; + } + } + } + + // Step 16. + return A; +} + +// ES2021 draft rev 190d474c3d8728653fbf8a5a37db1de34b9c1472 +// Plus <https://github.com/tc39/ecma262/pull/2221> +// 22.2.3.25 %TypedArray%.prototype.some ( callbackfn [ , thisArg ] ) +function TypedArraySome(callbackfn /*, thisArg*/) { + // Step 1. + var O = this; + + // Step 2. + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(O); + + // If we got here, `this` is either a typed array or a wrapper for one. + + // Step 3. + var len; + if (isTypedArray) { + len = TypedArrayLength(O); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + O, + "TypedArrayLengthMethod" + ); + } + + // Step 4. + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "%TypedArray%.prototype.some"); + } + if (!IsCallable(callbackfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, callbackfn)); + } + + var thisArg = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Steps 5-6. + for (var k = 0; k < len; k++) { + // Steps 6.a-b. + var kValue = O[k]; + + // Step 6.c. + var testResult = callContentFunction(callbackfn, thisArg, kValue, k, O); + + // Step 6.d. + if (testResult) { + return true; + } + } + + // Step 7. + return false; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(TypedArraySome); + +// To satisfy step 6.b from TypedArray SortCompare described in 23.2.3.29 the +// user supplied comparefn is wrapped. +function TypedArraySortCompare(comparefn) { + return function(x, y) { + // Step 6.b.i. + var v = +callContentFunction(comparefn, undefined, x, y); + + // Step 6.b.ii. + if (v !== v) { + return 0; + } + + // Step 6.b.iii. + return v; + }; +} + +// ES2019 draft rev 8a16cb8d18660a1106faae693f0f39b9f1a30748 +// 22.2.3.26 %TypedArray%.prototype.sort ( comparefn ) +function TypedArraySort(comparefn) { + // This function is not generic. + + // Step 1. + if (comparefn !== undefined) { + if (!IsCallable(comparefn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, comparefn)); + } + } + + // Step 2. + var obj = this; + + // Step 3. + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(obj); + + // Step 4. + var len; + if (isTypedArray) { + len = TypedArrayLength(obj); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + obj, + "TypedArrayLengthMethod" + ); + } + + // Arrays with less than two elements remain unchanged when sorted. + if (len <= 1) { + return obj; + } + + if (comparefn === undefined) { + return TypedArrayNativeSort(obj); + } + + // Steps 5-6. + var wrappedCompareFn = TypedArraySortCompare(comparefn); + + // Step 7. + var sorted = MergeSortTypedArray(obj, len, wrappedCompareFn); + + // Move the sorted elements into the array. + for (var i = 0; i < len; i++) { + obj[i] = sorted[i]; + } + + return obj; +} + +// ES2017 draft rev f8a9be8ea4bd97237d176907a1e3080dce20c68f +// 22.2.3.28 %TypedArray%.prototype.toLocaleString ([ reserved1 [ , reserved2 ] ]) +// ES2017 Intl draft rev 78bbe7d1095f5ff3760ac4017ed366026e4cb276 +// 13.4.1 Array.prototype.toLocaleString ([ locales [ , options ]]) +function TypedArrayToLocaleString(locales = undefined, options = undefined) { + // ValidateTypedArray, then step 1. + var array = this; + + // This function is not generic. + // We want to make sure that we have an attached buffer, per spec prose. + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(array); + + // If we got here, `this` is either a typed array or a wrapper for one. + + // Step 2. + var len; + if (isTypedArray) { + len = TypedArrayLength(array); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + array, + "TypedArrayLengthMethod" + ); + } + + // Step 4. + if (len === 0) { + return ""; + } + + // Step 5. + var firstElement = array[0]; + + // Steps 6-7. + // Omit the 'if' clause in step 6, since typed arrays can't have undefined + // or null elements. +#if JS_HAS_INTL_API + var R = ToString( + callContentFunction( + firstElement.toLocaleString, + firstElement, + locales, + options + ) + ); +#else + var R = ToString( + callContentFunction(firstElement.toLocaleString, firstElement) + ); +#endif + + // Step 3 (reordered). + // We don't (yet?) implement locale-dependent separators. + var separator = ","; + + // Steps 8-9. + for (var k = 1; k < len; k++) { + // Step 9.a. + var S = R + separator; + + // Step 9.b. + var nextElement = array[k]; + + // Step 9.c *should* be unreachable: typed array elements are numbers. + // But bug 1079853 means |nextElement| *could* be |undefined|, if the + // previous iteration's step 9.d or step 7 detached |array|'s buffer. + // Conveniently, if this happens, evaluating |nextElement.toLocaleString| + // throws the required TypeError, and the only observable difference is + // the error message. So despite bug 1079853, we can skip step 9.c. + + // Step 9.d. +#if JS_HAS_INTL_API + R = ToString( + callContentFunction( + nextElement.toLocaleString, + nextElement, + locales, + options + ) + ); +#else + R = ToString(callContentFunction(nextElement.toLocaleString, nextElement)); +#endif + + // Step 9.e. + R = S + R; + } + + // Step 10. + return R; +} + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 22.2.3.27 %TypedArray%.prototype.subarray ( begin, end ) +function TypedArraySubarray(begin, end) { + // Step 1. + var obj = this; + + // Steps 2-3. + // This function is not generic. + if (!IsObject(obj) || !IsTypedArray(obj)) { + return callFunction( + CallTypedArrayMethodIfWrapped, + this, + begin, + end, + "TypedArraySubarray" + ); + } + + // Step 4. + var buffer = ViewedArrayBufferIfReified(obj); + if (buffer === null) { + buffer = TypedArrayBuffer(obj); + } + + // Step 5. + var srcLength = TypedArrayLength(obj); + + // Step 13 (Reordered because otherwise it'd be observable that we reset + // the byteOffset to zero when the underlying array buffer gets detached). + var srcByteOffset = TypedArrayByteOffset(obj); + + // Step 6. + var relativeBegin = ToInteger(begin); + + // Step 7. + var beginIndex = + relativeBegin < 0 + ? std_Math_max(srcLength + relativeBegin, 0) + : std_Math_min(relativeBegin, srcLength); + + // Step 8. + var relativeEnd = end === undefined ? srcLength : ToInteger(end); + + // Step 9. + var endIndex = + relativeEnd < 0 + ? std_Math_max(srcLength + relativeEnd, 0) + : std_Math_min(relativeEnd, srcLength); + + // Step 10. + var newLength = std_Math_max(endIndex - beginIndex, 0); + + // Steps 11-12. + var elementSize = TypedArrayElementSize(obj); + + // Step 14. + var beginByteOffset = srcByteOffset + beginIndex * elementSize; + + // Steps 15-16. + return TypedArraySpeciesCreateWithBuffer( + obj, + buffer, + beginByteOffset, + newLength + ); +} + +// https://tc39.es/proposal-relative-indexing-method +// %TypedArray%.prototype.at ( index ) +function TypedArrayAt(index) { + // Step 1. + var obj = this; + + // Step 2. + // This function is not generic. + if (!IsObject(obj) || !IsTypedArray(obj)) { + return callFunction( + CallTypedArrayMethodIfWrapped, + obj, + index, + "TypedArrayAt" + ); + } + GetAttachedArrayBuffer(obj); + + // Step 3. + var len = TypedArrayLength(obj); + + // Step 4. + var relativeIndex = ToInteger(index); + + // Steps 5-6. + var k; + if (relativeIndex >= 0) { + k = relativeIndex; + } else { + k = len + relativeIndex; + } + + // Step 7. + if (k < 0 || k >= len) { + return undefined; + } + + // Step 8. + return obj[k]; +} +// This function is only barely too long for normal inlining. +SetIsInlinableLargeFunction(TypedArrayAt); + +// https://github.com/tc39/proposal-array-find-from-last +// %TypedArray%.prototype.findLast ( predicate, thisArg ) +function TypedArrayFindLast(predicate /*, thisArg*/) { + // Step 1. + var O = this; + + // Step 2. + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(O); + + // If we got here, `this` is either a typed array or a wrapper for one. + + // Step 3. + var len; + if (isTypedArray) { + len = TypedArrayLength(O); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + O, + "TypedArrayLengthMethod" + ); + } + + // Step 4. + if (ArgumentsLength() === 0) { + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, "%TypedArray%.prototype.findLast"); + } + if (!IsCallable(predicate)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, predicate)); + } + + var thisArg = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Steps 5-6. + for (var k = len - 1; k >= 0; k--) { + // Steps 6.a-b. + var kValue = O[k]; + + // Steps 6.c-d. + if (callContentFunction(predicate, thisArg, kValue, k, O)) { + return kValue; + } + } + + // Step 7. + return undefined; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(TypedArrayFindLast); + +// https://github.com/tc39/proposal-array-find-from-last +// %TypedArray%.prototype.findLastIndex ( predicate, thisArg ) +function TypedArrayFindLastIndex(predicate /*, thisArg*/) { + // Step 1. + var O = this; + + // Step 2. + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(O); + + // If we got here, `this` is either a typed array or a wrapper for one. + + // Step 3. + var len; + if (isTypedArray) { + len = TypedArrayLength(O); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + O, + "TypedArrayLengthMethod" + ); + } + + // Step 4. + if (ArgumentsLength() === 0) { + ThrowTypeError( + JSMSG_MISSING_FUN_ARG, + 0, + "%TypedArray%.prototype.findLastIndex" + ); + } + if (!IsCallable(predicate)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, predicate)); + } + + var thisArg = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Steps 5-6. + for (var k = len - 1; k >= 0; k--) { + // Steps 6.a-f. + if (callContentFunction(predicate, thisArg, O[k], k, O)) { + return k; + } + } + + // Step 7. + return -1; +} +// Inlining this enables inlining of the callback function. +SetIsInlinableLargeFunction(TypedArrayFindLastIndex); + +// ES6 draft rev30 (2014/12/24) 22.2.3.30 %TypedArray%.prototype.values() +// +// Uncloned functions with `$` prefix are allocated as extended function +// to store the original name in `SetCanonicalName`. +function $TypedArrayValues() { + // Step 1. + var O = this; + + // See the big comment in TypedArrayEntries for what we're doing here. + IsTypedArrayEnsuringArrayBuffer(O); + + // Step 7. + return CreateArrayIterator(O, ITEM_KIND_VALUE); +} +SetCanonicalName($TypedArrayValues, "values"); + +// ES2021 draft rev 190d474c3d8728653fbf8a5a37db1de34b9c1472 +// Plus <https://github.com/tc39/ecma262/pull/2221> +// 22.2.3.13 %TypedArray%.prototype.includes ( searchElement [ , fromIndex ] ) +function TypedArrayIncludes(searchElement, fromIndex = 0) { + // Step 2. + if (!IsObject(this) || !IsTypedArray(this)) { + return callFunction( + CallTypedArrayMethodIfWrapped, + this, + searchElement, + fromIndex, + "TypedArrayIncludes" + ); + } + + GetAttachedArrayBuffer(this); + + // Step 1. + var O = this; + + // Step 3. + var len = TypedArrayLength(O); + + // Step 4. + if (len === 0) { + return false; + } + + // Step 5. + var n = ToInteger(fromIndex); + + // Step 6. + assert(fromIndex !== undefined || n === 0, "ToInteger(undefined) is zero"); + + // Steps 7-10. + // Steps 7-8 are handled implicitly. + var k; + if (n >= 0) { + // Step 9.a + k = n; + } else { + // Step 10.a. + k = len + n; + + // Step 10.b. + if (k < 0) { + k = 0; + } + } + + // Step 11. + while (k < len) { + // Steps 11.a-b. + if (SameValueZero(searchElement, O[k])) { + return true; + } + + // Step 11.c. + k++; + } + + // Step 12. + return false; +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 22.2.2.1 %TypedArray%.from ( source [ , mapfn [ , thisArg ] ] ) +function TypedArrayStaticFrom(source, mapfn = undefined, thisArg = undefined) { + // Step 1. + var C = this; + + // Step 2. + if (!IsConstructor(C)) { + ThrowTypeError(JSMSG_NOT_CONSTRUCTOR, typeof C); + } + + // Step 3. + var mapping; + if (mapfn !== undefined) { + // Step 3.a. + if (!IsCallable(mapfn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(1, mapfn)); + } + + // Step 3.b. + mapping = true; + } else { + // Step 4. + mapping = false; + } + + // Step 5. + var T = thisArg; + + // Step 6. + // Inlined: GetMethod, steps 1-2. + var usingIterator = source[GetBuiltinSymbol("iterator")]; + + // Step 7. + // Inlined: GetMethod, step 3. + if (usingIterator !== undefined && usingIterator !== null) { + // Inlined: GetMethod, step 4. + if (!IsCallable(usingIterator)) { + ThrowTypeError(JSMSG_NOT_ITERABLE, DecompileArg(0, source)); + } + + // Try to take a fast path when there's no mapper function and the + // constructor is a built-in TypedArray constructor. + if (!mapping && IsTypedArrayConstructor(C) && IsObject(source)) { + // The source is a TypedArray using the default iterator. + if ( + usingIterator === $TypedArrayValues && + IsTypedArray(source) && + ArrayIteratorPrototypeOptimizable() + ) { + // Step 7.a. + // Omitted but we still need to throw if |source| was detached. + GetAttachedArrayBuffer(source); + + // Step 7.b. + var len = TypedArrayLength(source); + + // Step 7.c. + var targetObj = constructContentFunction(C, C, len); + + // Steps 7.d-f. + for (var k = 0; k < len; k++) { + targetObj[k] = source[k]; + } + + // Step 7.g. + return targetObj; + } + + // The source is a packed array using the default iterator. + if ( + usingIterator === $ArrayValues && + IsPackedArray(source) && + ArrayIteratorPrototypeOptimizable() + ) { + // Steps 7.b-c. + var targetObj = constructContentFunction(C, C, source.length); + + // Steps 7.a, 7.d-f. + TypedArrayInitFromPackedArray(targetObj, source); + + // Step 7.g. + return targetObj; + } + } + + // Step 7.a. + var values = IterableToList(source, usingIterator); + + // Step 7.b. + var len = values.length; + + // Step 7.c. + var targetObj = TypedArrayCreateWithLength(C, len); + + // Steps 7.d-e. + for (var k = 0; k < len; k++) { + // Step 7.e.ii. + var kValue = values[k]; + + // Steps 7.e.iii-iv. + var mappedValue = mapping + ? callContentFunction(mapfn, T, kValue, k) + : kValue; + + // Step 7.e.v. + targetObj[k] = mappedValue; + } + + // Step 7.f. + // Asserting that `values` is empty here would require removing them one by one from + // the list's start in the loop above. That would introduce unacceptable overhead. + // Additionally, the loop's logic is simple enough not to require the assert. + + // Step 7.g. + return targetObj; + } + + // Step 8 is an assertion: items is not an Iterator. Testing this is + // literally the very last thing we did, so we don't assert here. + + // Step 9. + var arrayLike = ToObject(source); + + // Step 10. + var len = ToLength(arrayLike.length); + + // Step 11. + var targetObj = TypedArrayCreateWithLength(C, len); + + // Steps 12-13. + for (var k = 0; k < len; k++) { + // Steps 13.a-b. + var kValue = arrayLike[k]; + + // Steps 13.c-d. + var mappedValue = mapping + ? callContentFunction(mapfn, T, kValue, k) + : kValue; + + // Step 13.e. + targetObj[k] = mappedValue; + } + + // Step 14. + return targetObj; +} + +// ES2017 draft rev 6859bb9ccaea9c6ede81d71e5320e3833b92cb3e +// 22.2.2.2 %TypedArray%.of ( ...items ) +function TypedArrayStaticOf(/*...items*/) { + // Step 1. + var len = ArgumentsLength(); + + // Step 2 (implicit). + + // Step 3. + var C = this; + + // Step 4. + if (!IsConstructor(C)) { + ThrowTypeError(JSMSG_NOT_CONSTRUCTOR, typeof C); + } + + // Step 5. + var newObj = TypedArrayCreateWithLength(C, len); + + // Steps 6-7. + for (var k = 0; k < len; k++) { + newObj[k] = GetArgument(k); + } + + // Step 8. + return newObj; +} + +// ES 2016 draft Mar 25, 2016 22.2.2.4. +function $TypedArraySpecies() { + // Step 1. + return this; +} +SetCanonicalName($TypedArraySpecies, "get [Symbol.species]"); + +// ES2018 draft rev 0525bb33861c7f4e9850f8a222c89642947c4b9c +// 22.2.2.1.1 Runtime Semantics: IterableToList( items, method ) +function IterableToList(items, method) { + // Step 1 (Inlined GetIterator). + + // 7.4.1 GetIterator, step 1. + assert(IsCallable(method), "method argument is a function"); + + // 7.4.1 GetIterator, step 2. + var iterator = callContentFunction(method, items); + + // 7.4.1 GetIterator, step 3. + if (!IsObject(iterator)) { + ThrowTypeError(JSMSG_GET_ITER_RETURNED_PRIMITIVE); + } + + // 7.4.1 GetIterator, step 4. + var nextMethod = iterator.next; + + // Step 2. + var values = []; + + // Steps 3-4. + var i = 0; + while (true) { + // Step 4.a. + var next = callContentFunction(nextMethod, iterator); + if (!IsObject(next)) { + ThrowTypeError(JSMSG_ITER_METHOD_RETURNED_PRIMITIVE, "next"); + } + + // Step 4.b. + if (next.done) { + break; + } + DefineDataProperty(values, i++, next.value); + } + + // Step 5. + return values; +} + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 24.1.4.3 ArrayBuffer.prototype.slice ( start, end ) +function ArrayBufferSlice(start, end) { + // Step 1. + var O = this; + + // Steps 2-3, + // This function is not generic. + if (!IsObject(O) || (O = GuardToArrayBuffer(O)) === null) { + return callFunction( + CallArrayBufferMethodIfWrapped, + this, + start, + end, + "ArrayBufferSlice" + ); + } + + // Step 4. + if (IsDetachedBuffer(O)) { + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + } + + // Step 5. + var len = ArrayBufferByteLength(O); + + // Step 6. + var relativeStart = ToInteger(start); + + // Step 7. + var first = + relativeStart < 0 + ? std_Math_max(len + relativeStart, 0) + : std_Math_min(relativeStart, len); + + // Step 8. + var relativeEnd = end === undefined ? len : ToInteger(end); + + // Step 9. + var final = + relativeEnd < 0 + ? std_Math_max(len + relativeEnd, 0) + : std_Math_min(relativeEnd, len); + + // Step 10. + var newLen = std_Math_max(final - first, 0); + + // Step 11 + var ctor = SpeciesConstructor(O, GetBuiltinConstructor("ArrayBuffer")); + + // Step 12. + var new_ = constructContentFunction(ctor, ctor, newLen); + + // Steps 13-15. + var isWrapped = false; + var newBuffer; + if ((newBuffer = GuardToArrayBuffer(new_)) !== null) { + // Step 15. + if (IsDetachedBuffer(newBuffer)) { + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + } + } else { + newBuffer = new_; + + // Steps 13-14. + if (!IsWrappedArrayBuffer(newBuffer)) { + ThrowTypeError(JSMSG_NON_ARRAY_BUFFER_RETURNED); + } + + isWrapped = true; + + // Step 15. + if ( + callFunction( + CallArrayBufferMethodIfWrapped, + newBuffer, + "IsDetachedBufferThis" + ) + ) { + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + } + } + + // Step 16. + if (newBuffer === O) { + ThrowTypeError(JSMSG_SAME_ARRAY_BUFFER_RETURNED); + } + + // Step 17. + var actualLen = PossiblyWrappedArrayBufferByteLength(newBuffer); + if (actualLen < newLen) { + ThrowTypeError(JSMSG_SHORT_ARRAY_BUFFER_RETURNED, newLen, actualLen); + } + + // Steps 18-19. + if (IsDetachedBuffer(O)) { + ThrowTypeError(JSMSG_TYPED_ARRAY_DETACHED); + } + + // Steps 20-22. + ArrayBufferCopyData(newBuffer, 0, O, first, newLen, isWrapped); + + // Step 23. + return newBuffer; +} + +function IsDetachedBufferThis() { + return IsDetachedBuffer(this); +} + +// ES 2016 draft Mar 25, 2016 24.1.3.3. +function $ArrayBufferSpecies() { + // Step 1. + return this; +} +SetCanonicalName($ArrayBufferSpecies, "get [Symbol.species]"); + +// Shared memory and atomics proposal (30 Oct 2016) +function $SharedArrayBufferSpecies() { + // Step 1. + return this; +} +SetCanonicalName($SharedArrayBufferSpecies, "get [Symbol.species]"); + +// ES2020 draft rev dc1e21c454bd316810be1c0e7af0131a2d7f38e9 +// 24.2.4.3 SharedArrayBuffer.prototype.slice ( start, end ) +function SharedArrayBufferSlice(start, end) { + // Step 1. + var O = this; + + // Steps 2-3. + // This function is not generic. + if (!IsObject(O) || (O = GuardToSharedArrayBuffer(O)) === null) { + return callFunction( + CallSharedArrayBufferMethodIfWrapped, + this, + start, + end, + "SharedArrayBufferSlice" + ); + } + + // Step 4. + var len = SharedArrayBufferByteLength(O); + + // Step 5. + var relativeStart = ToInteger(start); + + // Step 6. + var first = + relativeStart < 0 + ? std_Math_max(len + relativeStart, 0) + : std_Math_min(relativeStart, len); + + // Step 7. + var relativeEnd = end === undefined ? len : ToInteger(end); + + // Step 8. + var final = + relativeEnd < 0 + ? std_Math_max(len + relativeEnd, 0) + : std_Math_min(relativeEnd, len); + + // Step 9. + var newLen = std_Math_max(final - first, 0); + + // Step 10 + var ctor = SpeciesConstructor(O, GetBuiltinConstructor("SharedArrayBuffer")); + + // Step 11. + var new_ = constructContentFunction(ctor, ctor, newLen); + + // Steps 12-13. + var isWrapped = false; + var newObj; + if ((newObj = GuardToSharedArrayBuffer(new_)) === null) { + if (!IsWrappedSharedArrayBuffer(new_)) { + ThrowTypeError(JSMSG_NON_SHARED_ARRAY_BUFFER_RETURNED); + } + isWrapped = true; + newObj = new_; + } + + // Step 14. + if (newObj === O || SharedArrayBuffersMemorySame(newObj, O)) { + ThrowTypeError(JSMSG_SAME_SHARED_ARRAY_BUFFER_RETURNED); + } + + // Step 15. + var actualLen = PossiblyWrappedSharedArrayBufferByteLength(newObj); + if (actualLen < newLen) { + ThrowTypeError(JSMSG_SHORT_SHARED_ARRAY_BUFFER_RETURNED, newLen, actualLen); + } + + // Steps 16-18. + SharedArrayBufferCopyData(newObj, 0, O, first, newLen, isWrapped); + + // Step 19. + return newObj; +} + +// https://github.com/tc39/proposal-change-array-by-copy +function TypedArrayCreateSameType(exemplar, length) { + // Step 1. Assert: exemplar is an Object that has [[TypedArrayName]] and [[ContentType]] internal slots. + assert( + IsPossiblyWrappedTypedArray(exemplar), + "in TypedArrayCreateSameType, exemplar does not have a [[ContentType]] internal slot" + ); + + // Step 2. Let constructor be the intrinsic object listed in column one of Table 63 for exemplar.[[TypedArrayName]]. + let constructor = ConstructorForTypedArray(exemplar); + + // Step 4 omitted. Assert: result has [[TypedArrayName]] and [[ContentType]] internal slots. - guaranteed by the TypedArray implementation + // Step 5 omitted. Assert: result.[[ContentType]] is exemplar.[[ContentType]]. - guaranteed by the typed array implementation + + // Step 3. Let result be ? TypedArrayCreate(constructor, argumentList). + // Step 6. Return result. + return TypedArrayCreateWithLength(constructor, length); +} + +// https://github.com/tc39/proposal-change-array-by-copy +// TypedArray.prototype.toReversed() +function TypedArrayToReversed() { + // Step 2. Perform ? ValidateTypedArray(O). + if (!IsObject(this) || !IsTypedArray(this)) { + return callFunction( + CallTypedArrayMethodIfWrapped, + this, + "TypedArrayToReversed" + ); + } + + GetAttachedArrayBuffer(this); + + // Step 1. Let O be the this value. + var O = this; + + // Step 3. Let length be O.[[ArrayLength]]. + var len = TypedArrayLength(O); + + // Step 4. Let A be ? TypedArrayCreateSameType(O, « 𝔽(length) »). + var A = TypedArrayCreateSameType(O, len); + + // Step 5. Let k be 0. + // Step 6. Repeat, while k < length, + for (var k = 0; k < len; k++) { + // Step 5.a. Let from be ! ToString(𝔽(length - k - 1)). + var from = len - k - 1; + // Step 5.b. omitted - Let Pk be ! ToString(𝔽(k)). + // k coerced to String by property access + // Step 5.c. Let fromValue be ! Get(O, from). + var fromValue = O[from]; + // Step 5.d. Perform ! Set(A, k, kValue, true). + A[k] = fromValue; + } + + // Step 7. Return A. + return A; +} + +// https://github.com/tc39/proposal-change-array-by-copy +// TypedArray.prototype.with() +function TypedArrayWith(index, value) { + // Step 2. Perform ? ValidateTypedArray(O). + if (!IsObject(this) || !IsTypedArray(this)) { + return callFunction( + CallTypedArrayMethodIfWrapped, + this, + index, + value, + "TypedArrayWith" + ); + } + + GetAttachedArrayBuffer(this); + + // Step 1. Let O be the this value. + var O = this; + + // Step 3. Let len be O.[[ArrayLength]]. + var len = TypedArrayLength(O); + + // Step 4. Let relativeIndex be ? ToIntegerOrInfinity(index). + var relativeIndex = ToInteger(index); + + var actualIndex; + if (relativeIndex >= 0) { + // Step 5. If relativeIndex ≥ 0, let actualIndex be relativeIndex. + actualIndex = relativeIndex; + } else { + // Step 6. Else, let actualIndex be len + relativeIndex. + actualIndex = len + relativeIndex; + } + + var kind = GetTypedArrayKind(O); + if (kind === TYPEDARRAY_KIND_BIGINT64 || kind === TYPEDARRAY_KIND_BIGUINT64) { + // Step 7. If O.[[ContentType]] is BigInt, set value to ? ToBigInt(value). + value = ToBigInt(value); + } else { + // Step 8. Else, set value to ? ToNumber(value). + value = ToNumber(value); + } + + // Reload the array length in case the underlying buffer has been detached. + len = TypedArrayLength(O); + assert( + !IsDetachedBuffer(ViewedArrayBufferIfReified(O)) || len === 0, + "length is set to zero when the buffer has been detached" + ); + + // Step 9. If ! IsValidIntegerIndex(O, 𝔽(actualIndex)) is false, throw a RangeError exception. + // This check is an inlined version of the IsValidIntegerIndex abstract operation. + if (actualIndex < 0 || actualIndex >= len) { + ThrowRangeError(JSMSG_BAD_INDEX); + } + + // Step 10. Let A be ? TypedArrayCreateSameType(O, « 𝔽(len) »). + var A = TypedArrayCreateSameType(O, len); + + // Step 11. Let k be 0. + // Step 12. Repeat, while k < len, + for (var k = 0; k < len; k++) { + // Step 12.a. omitted - Let Pk be ! ToString(𝔽(k)). + // k coerced to String by property access + + // Step 12.b. If k is actualIndex, let fromValue be value. + // Step 12.c. Else, let fromValue be ! Get(O, Pk). + var fromValue = k === actualIndex ? value : O[k]; + + // Step 12.d. Perform ! Set(A, Pk, fromValue, true). + A[k] = fromValue; + } + + // Step 13. + return A; +} + +// https://github.com/tc39/proposal-change-array-by-copy +// TypedArray.prototype.toSorted() +function TypedArrayToSorted(comparefn) { + // Step 1. If comparefn is not undefined and IsCallable(comparefn) is false, throw a TypeError exception. + if (comparefn !== undefined) { + if (!IsCallable(comparefn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, comparefn)); + } + } + + // Step 2. Let O be the this value. + var O = this; + + // Step 3. Perform ? ValidateTypedArray(this). + var isTypedArray = IsTypedArrayEnsuringArrayBuffer(O); + + // Step 4. omitted. Let buffer be obj.[[ViewedArrayBuffer]]. + // FIXME: Draft spec not synched with https://github.com/tc39/ecma262/pull/2723 + + // Step 5. Let len be O.[[ArrayLength]]. + var len; + if (isTypedArray) { + len = TypedArrayLength(O); + } else { + len = callFunction( + CallTypedArrayMethodIfWrapped, + O, + "TypedArrayLengthMethod" + ); + } + + // Arrays with less than two elements remain unchanged when sorted. + if (len <= 1) { + // Step 6. Let A be ? TypedArrayCreateSameType(O, « 𝔽(len) »). + var A = TypedArrayCreateSameType(O, len); + + // Steps 7-11. + if (len > 0) { + A[0] = O[0]; + } + + // Step 12. + return A; + } + + if (comparefn === undefined) { + // Step 6. Let A be ? TypedArrayCreateSameType(O, « 𝔽(len) »). + var A = TypedArrayCreateSameType(O, len); + + // Steps 7-11 not followed exactly; this implementation copies the list and then + // sorts the copy, rather than calling a sort method that copies the list and then + // copying the result again. + + // Equivalent to steps 10-11. + for (var k = 0; k < len; k++) { + A[k] = O[k]; + } + + // Equivalent to steps 7-9 and 12. + return TypedArrayNativeSort(A); + } + + // Steps 7-8. + var wrappedCompareFn = TypedArraySortCompare(comparefn); + + // Steps 6 and 9-12. + // + // MergeSortTypedArray returns a sorted copy - exactly what we need to return. + return MergeSortTypedArray(O, len, wrappedCompareFn); +} diff --git a/js/src/builtin/TypedArrayConstants.h b/js/src/builtin/TypedArrayConstants.h new file mode 100644 index 0000000000..a41243e4c9 --- /dev/null +++ b/js/src/builtin/TypedArrayConstants.h @@ -0,0 +1,24 @@ +/* -*- 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/. */ + +// Specialized .h file to be used by both JS and C++ code. + +#ifndef builtin_TypedArrayConstants_h +#define builtin_TypedArrayConstants_h + +/////////////////////////////////////////////////////////////////////////// +// Slots for objects using the typed array layout + +#define JS_TYPEDARRAYLAYOUT_BUFFER_SLOT 0 + +/////////////////////////////////////////////////////////////////////////// +// Slots and flags for ArrayBuffer objects + +#define JS_ARRAYBUFFER_FLAGS_SLOT 3 + +#define JS_ARRAYBUFFER_DETACHED_FLAG 0x8 + +#endif diff --git a/js/src/builtin/Utilities.js b/js/src/builtin/Utilities.js new file mode 100644 index 0000000000..dad4e26bb9 --- /dev/null +++ b/js/src/builtin/Utilities.js @@ -0,0 +1,242 @@ +/* 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 "SelfHostingDefines.h" + +// Assertions and debug printing, defined here instead of in the header above +// to make `assert` invisible to C++. +#ifdef DEBUG +#define assert(b, info) \ + do { \ + if (!(b)) { \ + AssertionFailed(__FILE__ + ":" + __LINE__ + ": " + info) \ + } \ + } while (false) +#define dbg(msg) \ + do { \ + DumpMessage(callFunction(std_Array_pop, \ + StringSplitString(__FILE__, '/')) + \ + '#' + __LINE__ + ': ' + msg) \ + } while (false) +#else +#define assert(b, info) ; // Elided assertion. +#define dbg(msg) ; // Elided debugging output. +#endif + +// All C++-implemented standard builtins library functions used in self-hosted +// code are installed via the std_functions JSFunctionSpec[] in +// SelfHosting.cpp. + +/********** Specification types **********/ + +// A "Record" is an internal type used in the ECMAScript spec to define a struct +// made up of key / values. It is never exposed to user script, but we use a +// simple Object (with null prototype) as a convenient implementation. +function new_Record() { + return std_Object_create(null); +} + +/********** Abstract operations defined in ECMAScript Language Specification **********/ + +/* Spec: ECMAScript Language Specification, 5.1 edition, 9.2 and 11.4.9 */ +function ToBoolean(v) { + return !!v; +} + +/* Spec: ECMAScript Language Specification, 5.1 edition, 9.3 and 11.4.6 */ +function ToNumber(v) { + return +v; +} + +// ES2017 draft rev aebf014403a3e641fb1622aec47c40f051943527 +// 7.2.10 SameValueZero ( x, y ) +function SameValueZero(x, y) { + return x === y || (x !== x && y !== y); +} + +// ES 2017 draft (April 6, 2016) 7.3.9 +function GetMethod(V, P) { + // Step 1. + assert(IsPropertyKey(P), "Invalid property key"); + + // Step 2. + var func = V[P]; + + // Step 3. + if (IsNullOrUndefined(func)) { + return undefined; + } + + // Step 4. + if (!IsCallable(func)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, typeof func); + } + + // Step 5. + return func; +} + +/* Spec: ECMAScript Draft, 6th edition Dec 24, 2014, 7.2.7 */ +function IsPropertyKey(argument) { + var type = typeof argument; + return type === "string" || type === "symbol"; +} + +#define TO_PROPERTY_KEY(name) \ +(typeof name !== "string" && typeof name !== "number" && typeof name !== "symbol" ? ToPropertyKey(name) : name) + +// ES 2016 draft Mar 25, 2016 7.3.20. +function SpeciesConstructor(obj, defaultConstructor) { + // Step 1. + assert(IsObject(obj), "not passed an object"); + + // Step 2. + var ctor = obj.constructor; + + // Step 3. + if (ctor === undefined) { + return defaultConstructor; + } + + // Step 4. + if (!IsObject(ctor)) { + ThrowTypeError(JSMSG_OBJECT_REQUIRED, "object's 'constructor' property"); + } + + // Steps 5. + var s = ctor[GetBuiltinSymbol("species")]; + + // Step 6. + if (IsNullOrUndefined(s)) { + return defaultConstructor; + } + + // Step 7. + if (IsConstructor(s)) { + return s; + } + + // Step 8. + ThrowTypeError( + JSMSG_NOT_CONSTRUCTOR, + "@@species property of object's constructor" + ); +} + +function GetTypeError(...args) { + try { + FUN_APPLY(ThrowTypeError, undefined, args); + } catch (e) { + return e; + } + assert(false, "the catch block should've returned from this function."); +} + +function GetAggregateError(...args) { + try { + FUN_APPLY(ThrowAggregateError, undefined, args); + } catch (e) { + return e; + } + assert(false, "the catch block should've returned from this function."); +} + +function GetInternalError(...args) { + try { + FUN_APPLY(ThrowInternalError, undefined, args); + } catch (e) { + return e; + } + assert(false, "the catch block should've returned from this function."); +} + +// To be used when a function is required but calling it shouldn't do anything. +function NullFunction() {} + +// ES2019 draft rev 4c2df13f4194057f09b920ee88712e5a70b1a556 +// 7.3.23 CopyDataProperties (target, source, excludedItems) +function CopyDataProperties(target, source, excludedItems) { + // Step 1. + assert(IsObject(target), "target is an object"); + + // Step 2. + assert(IsObject(excludedItems), "excludedItems is an object"); + + // Steps 3 and 7. + if (IsNullOrUndefined(source)) { + return; + } + + // Step 4. + var from = ToObject(source); + + // Step 5. + var keys = CopyDataPropertiesOrGetOwnKeys(target, from, excludedItems); + + // Return if we copied all properties in native code. + if (keys === null) { + return; + } + + // Step 6. + for (var index = 0; index < keys.length; index++) { + var key = keys[index]; + + // We abbreviate this by calling propertyIsEnumerable which is faster + // and returns false for not defined properties. + if ( + !hasOwn(key, excludedItems) && + callFunction(std_Object_propertyIsEnumerable, from, key) + ) { + DefineDataProperty(target, key, from[key]); + } + } + + // Step 7 (Return). +} + +// ES2019 draft rev 4c2df13f4194057f09b920ee88712e5a70b1a556 +// 7.3.23 CopyDataProperties (target, source, excludedItems) +function CopyDataPropertiesUnfiltered(target, source) { + // Step 1. + assert(IsObject(target), "target is an object"); + + // Step 2 (Not applicable). + + // Steps 3 and 7. + if (IsNullOrUndefined(source)) { + return; + } + + // Step 4. + var from = ToObject(source); + + // Step 5. + var keys = CopyDataPropertiesOrGetOwnKeys(target, from, null); + + // Return if we copied all properties in native code. + if (keys === null) { + return; + } + + // Step 6. + for (var index = 0; index < keys.length; index++) { + var key = keys[index]; + + // We abbreviate this by calling propertyIsEnumerable which is faster + // and returns false for not defined properties. + if (callFunction(std_Object_propertyIsEnumerable, from, key)) { + DefineDataProperty(target, key, from[key]); + } + } + + // Step 7 (Return). +} + +/*************************************** Testing functions ***************************************/ +function outer() { + return function inner() { + return "foo"; + }; +} diff --git a/js/src/builtin/WeakMap.js b/js/src/builtin/WeakMap.js new file mode 100644 index 0000000000..5e91c5eba6 --- /dev/null +++ b/js/src/builtin/WeakMap.js @@ -0,0 +1,28 @@ +/* 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/. */ + +// ES2017 draft rev 0e10c9f29fca1385980c08a7d5e7bb3eb775e2e4 +// 23.3.1.1 WeakMap, steps 6-8 +function WeakMapConstructorInit(iterable) { + var map = this; + + // Step 6.a. + var adder = map.set; + + // Step 6.b. + if (!IsCallable(adder)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, typeof adder); + } + + // Steps 6.c-8. + for (var nextItem of allowContentIter(iterable)) { + // Step 8.d. + if (!IsObject(nextItem)) { + ThrowTypeError(JSMSG_INVALID_MAP_ITERABLE, "WeakMap"); + } + + // Steps 8.e-j. + callContentFunction(adder, map, nextItem[0], nextItem[1]); + } +} diff --git a/js/src/builtin/WeakMapObject-inl.h b/js/src/builtin/WeakMapObject-inl.h new file mode 100644 index 0000000000..7dbe5cf8e5 --- /dev/null +++ b/js/src/builtin/WeakMapObject-inl.h @@ -0,0 +1,65 @@ +/* -*- 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 builtin_WeakMapObject_inl_h +#define builtin_WeakMapObject_inl_h + +#include "builtin/WeakMapObject.h" + +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/Wrapper.h" +#include "gc/WeakMap-inl.h" +#include "vm/JSObject-inl.h" + +namespace js { + +static bool TryPreserveReflector(JSContext* cx, HandleObject obj) { + if (!MaybePreserveDOMWrapper(cx, obj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_WEAKMAP_KEY); + return false; + } + + return true; +} + +static MOZ_ALWAYS_INLINE bool WeakCollectionPutEntryInternal( + JSContext* cx, Handle<WeakCollectionObject*> obj, HandleObject key, + HandleValue value) { + ObjectValueWeakMap* map = obj->getMap(); + if (!map) { + auto newMap = cx->make_unique<ObjectValueWeakMap>(cx, obj.get()); + if (!newMap) { + return false; + } + map = newMap.release(); + InitReservedSlot(obj, WeakCollectionObject::DataSlot, map, + MemoryUse::WeakMapObject); + } + + // Preserve wrapped native keys to prevent wrapper optimization. + if (!TryPreserveReflector(cx, key)) { + return false; + } + + RootedObject delegate(cx, UncheckedUnwrapWithoutExpose(key)); + if (delegate && !TryPreserveReflector(cx, delegate)) { + return false; + } + + MOZ_ASSERT(key->compartment() == obj->compartment()); + MOZ_ASSERT_IF(value.isObject(), + value.toObject().compartment() == obj->compartment()); + if (!map->put(key, value)) { + JS_ReportOutOfMemory(cx); + return false; + } + return true; +} + +} // namespace js + +#endif /* builtin_WeakMapObject_inl_h */ diff --git a/js/src/builtin/WeakMapObject.cpp b/js/src/builtin/WeakMapObject.cpp new file mode 100644 index 0000000000..47c1af4431 --- /dev/null +++ b/js/src/builtin/WeakMapObject.cpp @@ -0,0 +1,309 @@ +/* -*- 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 "builtin/WeakMapObject-inl.h" + +#include "builtin/WeakSetObject.h" +#include "gc/GC.h" +#include "gc/GCContext.h" +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "js/PropertySpec.h" +#include "js/WeakMap.h" +#include "vm/Compartment.h" +#include "vm/JSContext.h" +#include "vm/SelfHosting.h" + +#include "gc/GCContext-inl.h" +#include "gc/WeakMap-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +/* static */ MOZ_ALWAYS_INLINE bool WeakMapObject::is(HandleValue v) { + return v.isObject() && v.toObject().is<WeakMapObject>(); +} + +/* static */ MOZ_ALWAYS_INLINE bool WeakMapObject::has_impl( + JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + if (!args.get(0).isObject()) { + args.rval().setBoolean(false); + return true; + } + + if (ObjectValueWeakMap* map = + args.thisv().toObject().as<WeakMapObject>().getMap()) { + JSObject* key = &args[0].toObject(); + if (map->has(key)) { + args.rval().setBoolean(true); + return true; + } + } + + args.rval().setBoolean(false); + return true; +} + +/* static */ +bool WeakMapObject::has(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<WeakMapObject::is, WeakMapObject::has_impl>(cx, + args); +} + +/* static */ MOZ_ALWAYS_INLINE bool WeakMapObject::get_impl( + JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(WeakMapObject::is(args.thisv())); + + if (!args.get(0).isObject()) { + args.rval().setUndefined(); + return true; + } + + if (ObjectValueWeakMap* map = + args.thisv().toObject().as<WeakMapObject>().getMap()) { + JSObject* key = &args[0].toObject(); + if (ObjectValueWeakMap::Ptr ptr = map->lookup(key)) { + args.rval().set(ptr->value()); + return true; + } + } + + args.rval().setUndefined(); + return true; +} + +/* static */ +bool WeakMapObject::get(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<WeakMapObject::is, WeakMapObject::get_impl>(cx, + args); +} + +/* static */ MOZ_ALWAYS_INLINE bool WeakMapObject::delete_impl( + JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(WeakMapObject::is(args.thisv())); + + if (!args.get(0).isObject()) { + args.rval().setBoolean(false); + return true; + } + + if (ObjectValueWeakMap* map = + args.thisv().toObject().as<WeakMapObject>().getMap()) { + JSObject* key = &args[0].toObject(); + // The lookup here is only used for the removal, so we can skip the read + // barrier. This is not very important for performance, but makes it easier + // to test nonbarriered removal from internal weakmaps (eg Debugger maps.) + if (ObjectValueWeakMap::Ptr ptr = map->lookupUnbarriered(key)) { + map->remove(ptr); + args.rval().setBoolean(true); + return true; + } + } + + args.rval().setBoolean(false); + return true; +} + +/* static */ +bool WeakMapObject::delete_(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<WeakMapObject::is, WeakMapObject::delete_impl>( + cx, args); +} + +/* static */ MOZ_ALWAYS_INLINE bool WeakMapObject::set_impl( + JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(WeakMapObject::is(args.thisv())); + + if (!args.get(0).isObject()) { + ReportNotObject(cx, JSMSG_OBJECT_REQUIRED_WEAKMAP_KEY, args.get(0)); + return false; + } + + RootedObject key(cx, &args[0].toObject()); + Rooted<WeakMapObject*> map(cx, &args.thisv().toObject().as<WeakMapObject>()); + + if (!WeakCollectionPutEntryInternal(cx, map, key, args.get(1))) { + return false; + } + args.rval().set(args.thisv()); + return true; +} + +/* static */ +bool WeakMapObject::set(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<WeakMapObject::is, WeakMapObject::set_impl>(cx, + args); +} + +size_t WeakCollectionObject::sizeOfExcludingThis( + mozilla::MallocSizeOf aMallocSizeOf) { + ObjectValueWeakMap* map = getMap(); + return map ? map->sizeOfIncludingThis(aMallocSizeOf) : 0; +} + +bool WeakCollectionObject::nondeterministicGetKeys( + JSContext* cx, Handle<WeakCollectionObject*> obj, MutableHandleObject ret) { + RootedObject arr(cx, NewDenseEmptyArray(cx)); + if (!arr) { + return false; + } + if (ObjectValueWeakMap* map = obj->getMap()) { + // Prevent GC from mutating the weakmap while iterating. + gc::AutoSuppressGC suppress(cx); + for (ObjectValueWeakMap::Base::Range r = map->all(); !r.empty(); + r.popFront()) { + JS::ExposeObjectToActiveJS(r.front().key()); + RootedObject key(cx, r.front().key()); + if (!cx->compartment()->wrap(cx, &key)) { + return false; + } + if (!NewbornArrayPush(cx, arr, ObjectValue(*key))) { + return false; + } + } + } + ret.set(arr); + return true; +} + +JS_PUBLIC_API bool JS_NondeterministicGetWeakMapKeys(JSContext* cx, + HandleObject objArg, + MutableHandleObject ret) { + RootedObject obj(cx, UncheckedUnwrap(objArg)); + if (!obj || !obj->is<WeakMapObject>()) { + ret.set(nullptr); + return true; + } + return WeakCollectionObject::nondeterministicGetKeys( + cx, obj.as<WeakCollectionObject>(), ret); +} + +static void WeakCollection_trace(JSTracer* trc, JSObject* obj) { + if (ObjectValueWeakMap* map = obj->as<WeakCollectionObject>().getMap()) { + map->trace(trc); + } +} + +static void WeakCollection_finalize(JS::GCContext* gcx, JSObject* obj) { + if (ObjectValueWeakMap* map = obj->as<WeakCollectionObject>().getMap()) { + gcx->delete_(obj, map, MemoryUse::WeakMapObject); + } +} + +JS_PUBLIC_API JSObject* JS::NewWeakMapObject(JSContext* cx) { + return NewBuiltinClassInstance<WeakMapObject>(cx); +} + +JS_PUBLIC_API bool JS::IsWeakMapObject(JSObject* obj) { + return obj->is<WeakMapObject>(); +} + +JS_PUBLIC_API bool JS::GetWeakMapEntry(JSContext* cx, HandleObject mapObj, + HandleObject key, + MutableHandleValue rval) { + CHECK_THREAD(cx); + cx->check(key); + rval.setUndefined(); + ObjectValueWeakMap* map = mapObj->as<WeakMapObject>().getMap(); + if (!map) { + return true; + } + if (ObjectValueWeakMap::Ptr ptr = map->lookup(key)) { + // Read barrier to prevent an incorrectly gray value from escaping the + // weak map. See the comment before UnmarkGrayChildren in gc/Marking.cpp + ExposeValueToActiveJS(ptr->value().get()); + rval.set(ptr->value()); + } + return true; +} + +JS_PUBLIC_API bool JS::SetWeakMapEntry(JSContext* cx, HandleObject mapObj, + HandleObject key, HandleValue val) { + CHECK_THREAD(cx); + cx->check(key, val); + Handle<WeakMapObject*> rootedMap = mapObj.as<WeakMapObject>(); + return WeakCollectionPutEntryInternal(cx, rootedMap, key, val); +} + +/* static */ +bool WeakMapObject::construct(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // ES6 draft rev 31 (15 Jan 2015) 23.3.1.1 step 1. + if (!ThrowIfNotConstructing(cx, args, "WeakMap")) { + return false; + } + + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_WeakMap, &proto)) { + return false; + } + + RootedObject obj(cx, NewObjectWithClassProto<WeakMapObject>(cx, proto)); + if (!obj) { + return false; + } + + // Steps 5-6, 11. + if (!args.get(0).isNullOrUndefined()) { + FixedInvokeArgs<1> args2(cx); + args2[0].set(args[0]); + + RootedValue thisv(cx, ObjectValue(*obj)); + if (!CallSelfHostedFunction(cx, cx->names().WeakMapConstructorInit, thisv, + args2, args2.rval())) { + return false; + } + } + + args.rval().setObject(*obj); + return true; +} + +const JSClassOps WeakCollectionObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + WeakCollection_finalize, // finalize + nullptr, // call + nullptr, // construct + WeakCollection_trace, // trace +}; + +const ClassSpec WeakMapObject::classSpec_ = { + GenericCreateConstructor<WeakMapObject::construct, 0, + gc::AllocKind::FUNCTION>, + GenericCreatePrototype<WeakMapObject>, + nullptr, + nullptr, + WeakMapObject::methods, + WeakMapObject::properties, +}; + +const JSClass WeakMapObject::class_ = { + "WeakMap", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | + JSCLASS_HAS_CACHED_PROTO(JSProto_WeakMap) | JSCLASS_BACKGROUND_FINALIZE, + &WeakCollectionObject::classOps_, &WeakMapObject::classSpec_}; + +const JSClass WeakMapObject::protoClass_ = { + "WeakMap.prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_WeakMap), + JS_NULL_CLASS_OPS, &WeakMapObject::classSpec_}; + +const JSPropertySpec WeakMapObject::properties[] = { + JS_STRING_SYM_PS(toStringTag, "WeakMap", JSPROP_READONLY), JS_PS_END}; + +const JSFunctionSpec WeakMapObject::methods[] = { + JS_FN("has", has, 1, 0), JS_FN("get", get, 1, 0), + JS_FN("delete", delete_, 1, 0), JS_FN("set", set, 2, 0), JS_FS_END}; diff --git a/js/src/builtin/WeakMapObject.h b/js/src/builtin/WeakMapObject.h new file mode 100644 index 0000000000..4b84141093 --- /dev/null +++ b/js/src/builtin/WeakMapObject.h @@ -0,0 +1,65 @@ +/* -*- 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 builtin_WeakMapObject_h +#define builtin_WeakMapObject_h + +#include "gc/WeakMap.h" +#include "vm/NativeObject.h" + +namespace js { + +// Abstract base class for WeakMapObject and WeakSetObject. +class WeakCollectionObject : public NativeObject { + public: + enum { DataSlot, SlotCount }; + + ObjectValueWeakMap* getMap() { + return maybePtrFromReservedSlot<ObjectValueWeakMap>(DataSlot); + } + + size_t sizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf); + + [[nodiscard]] static bool nondeterministicGetKeys( + JSContext* cx, Handle<WeakCollectionObject*> obj, + MutableHandleObject ret); + + protected: + static const JSClassOps classOps_; +}; + +class WeakMapObject : public WeakCollectionObject { + public: + static const JSClass class_; + static const JSClass protoClass_; + + private: + static const ClassSpec classSpec_; + + static const JSPropertySpec properties[]; + static const JSFunctionSpec methods[]; + + [[nodiscard]] static bool construct(JSContext* cx, unsigned argc, Value* vp); + + [[nodiscard]] static MOZ_ALWAYS_INLINE bool is(HandleValue v); + + [[nodiscard]] static MOZ_ALWAYS_INLINE bool has_impl(JSContext* cx, + const CallArgs& args); + [[nodiscard]] static bool has(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static MOZ_ALWAYS_INLINE bool get_impl(JSContext* cx, + const CallArgs& args); + [[nodiscard]] static bool get(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static MOZ_ALWAYS_INLINE bool delete_impl(JSContext* cx, + const CallArgs& args); + [[nodiscard]] static bool delete_(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static MOZ_ALWAYS_INLINE bool set_impl(JSContext* cx, + const CallArgs& args); + [[nodiscard]] static bool set(JSContext* cx, unsigned argc, Value* vp); +}; + +} // namespace js + +#endif /* builtin_WeakMapObject_h */ diff --git a/js/src/builtin/WeakRefObject.cpp b/js/src/builtin/WeakRefObject.cpp new file mode 100644 index 0000000000..647b5e2601 --- /dev/null +++ b/js/src/builtin/WeakRefObject.cpp @@ -0,0 +1,265 @@ +/* -*- 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 "builtin/WeakRefObject.h" + +#include "jsapi.h" + +#include "gc/FinalizationObservers.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" + +#include "gc/PrivateIterators-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +namespace js { + +/* static */ +bool WeakRefObject::construct(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // https://tc39.es/proposal-weakrefs/#sec-weak-ref-constructor + // The WeakRef constructor is not intended to be called as a function and will + // throw an exception when called in that manner. + if (!ThrowIfNotConstructing(cx, args, "WeakRef")) { + return false; + } + + // https://tc39.es/proposal-weakrefs/#sec-weak-ref-target + // 1. If NewTarget is undefined, throw a TypeError exception. + // 2. If Type(target) is not Object, throw a TypeError exception. + if (!args.get(0).isObject()) { + ReportNotObject(cx, args.get(0)); + return false; + } + + // 3. Let weakRef be ? OrdinaryCreateFromConstructor(NewTarget, + // "%WeakRefPrototype%", « [[Target]] »). + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_WeakRef, &proto)) { + return false; + } + + Rooted<WeakRefObject*> weakRef( + cx, NewObjectWithClassProto<WeakRefObject>(cx, proto)); + if (!weakRef) { + return false; + } + + RootedObject target(cx); + target = CheckedUnwrapDynamic(&args[0].toObject(), cx); + if (!target) { + ReportAccessDenied(cx); + return false; + } + + // If the target is a DOM wrapper, preserve it. + if (!preserveDOMWrapper(cx, target)) { + return false; + } + + // Wrap the weakRef into the target's Zone. This is a cross-compartment + // wrapper if the Zone is different, or same-compartment (the original + // object) if the Zone is the same *even if* the compartments are different. + RootedObject wrappedWeakRef(cx, weakRef); + bool sameZone = target->zone() == weakRef->zone(); + AutoRealm ar(cx, sameZone ? weakRef : target); + if (!JS_WrapObject(cx, &wrappedWeakRef)) { + return false; + } + + if (JS_IsDeadWrapper(wrappedWeakRef)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEAD_OBJECT); + return false; + } + + // 4. Perfom ! KeepDuringJob(target). + if (!target->zone()->keepDuringJob(target)) { + ReportOutOfMemory(cx); + return false; + }; + + // Add an entry to the per-zone maps from target JS object to a list of weak + // ref objects. + gc::GCRuntime* gc = &cx->runtime()->gc; + if (!gc->registerWeakRef(target, wrappedWeakRef)) { + ReportOutOfMemory(cx); + return false; + }; + + // 5. Set weakRef.[[Target]] to target. + weakRef->setReservedSlotGCThingAsPrivate(TargetSlot, target); + + // 6. Return weakRef. + args.rval().setObject(*weakRef); + return true; +} + +/* static */ +bool WeakRefObject::preserveDOMWrapper(JSContext* cx, HandleObject obj) { + if (!MaybePreserveDOMWrapper(cx, obj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_BAD_WEAKREF_TARGET); + return false; + } + + return true; +} + +/* static */ +void WeakRefObject::trace(JSTracer* trc, JSObject* obj) { + WeakRefObject* weakRef = &obj->as<WeakRefObject>(); + + if (trc->traceWeakEdges()) { + JSObject* target = weakRef->target(); + if (target) { + TraceManuallyBarrieredEdge(trc, &target, "WeakRefObject::target"); + weakRef->setTargetUnbarriered(target); + } + } +} + +/* static */ +void WeakRefObject::finalize(JS::GCContext* gcx, JSObject* obj) { + // The target is cleared when the target's zone is swept and that always + // happens before this object is finalized because of the CCW from the target + // zone to this object. If the CCW is nuked, the target is cleared in + // NotifyGCNukeWrapper(). + MOZ_ASSERT(!obj->as<WeakRefObject>().target()); +} + +const JSClassOps WeakRefObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + finalize, // finalize + nullptr, // call + nullptr, // construct + trace, // trace +}; + +const ClassSpec WeakRefObject::classSpec_ = { + GenericCreateConstructor<WeakRefObject::construct, 1, + gc::AllocKind::FUNCTION>, + GenericCreatePrototype<WeakRefObject>, + nullptr, + nullptr, + WeakRefObject::methods, + WeakRefObject::properties, +}; + +const JSClass WeakRefObject::class_ = { + "WeakRef", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | + JSCLASS_HAS_CACHED_PROTO(JSProto_WeakRef) | JSCLASS_FOREGROUND_FINALIZE, + &classOps_, &classSpec_}; + +const JSClass WeakRefObject::protoClass_ = { + // https://tc39.es/proposal-weakrefs/#sec-weak-ref.prototype + // https://tc39.es/proposal-weakrefs/#sec-properties-of-the-weak-ref-prototype-object + "WeakRef.prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_WeakRef), + JS_NULL_CLASS_OPS, &classSpec_}; + +const JSPropertySpec WeakRefObject::properties[] = { + JS_STRING_SYM_PS(toStringTag, "WeakRef", JSPROP_READONLY), JS_PS_END}; + +const JSFunctionSpec WeakRefObject::methods[] = {JS_FN("deref", deref, 0, 0), + JS_FS_END}; + +/* static */ +bool WeakRefObject::deref(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // https://tc39.es/proposal-weakrefs/#sec-weak-ref.prototype.deref + // 1. Let weakRef be the this value. + // 2. If Type(weakRef) is not Object, throw a TypeError exception. + // 3. If weakRef does not have a [[Target]] internal slot, throw a TypeError + // exception. + if (!args.thisv().isObject() || + !args.thisv().toObject().is<WeakRefObject>()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NOT_A_WEAK_REF, + "Receiver of WeakRef.deref call"); + return false; + } + + Rooted<WeakRefObject*> weakRef(cx, + &args.thisv().toObject().as<WeakRefObject>()); + + // We need to perform a read barrier, which may clear the target. + readBarrier(cx, weakRef); + + // 4. Let target be the value of weakRef.[[Target]]. + // 5. If target is not empty, + // a. Perform ! KeepDuringJob(target). + // b. Return target. + // 6. Return undefined. + if (!weakRef->target()) { + args.rval().setUndefined(); + return true; + } + + RootedObject target(cx, weakRef->target()); + if (!target->zone()->keepDuringJob(target)) { + return false; + } + + // Target should be wrapped into the current realm before returning it. + RootedObject wrappedTarget(cx, target); + if (!JS_WrapObject(cx, &wrappedTarget)) { + return false; + } + + args.rval().setObject(*wrappedTarget); + return true; +} + +void WeakRefObject::setTargetUnbarriered(JSObject* target) { + setReservedSlotGCThingAsPrivateUnbarriered(TargetSlot, target); +} + +void WeakRefObject::clearTarget() { + clearReservedSlotGCThingAsPrivate(TargetSlot); +} + +/* static */ +void WeakRefObject::readBarrier(JSContext* cx, Handle<WeakRefObject*> self) { + RootedObject obj(cx, self->target()); + if (!obj) { + return; + } + + if (obj->getClass()->isDOMClass()) { + // We preserved the target when the WeakRef was created. If it has since + // been released then the DOM object it wraps has been collected, so clear + // the target. + MOZ_ASSERT(cx->runtime()->hasReleasedWrapperCallback); + bool wasReleased = cx->runtime()->hasReleasedWrapperCallback(obj); + if (wasReleased) { + obj->zone()->finalizationObservers()->removeWeakRefTarget(obj, self); + return; + } + } + + gc::ReadBarrier(obj.get()); +} + +namespace gc { + +void GCRuntime::traceKeptObjects(JSTracer* trc) { + for (GCZonesIter zone(this); !zone.done(); zone.next()) { + zone->traceKeptObjects(trc); + } +} + +} // namespace gc + +} // namespace js diff --git a/js/src/builtin/WeakRefObject.h b/js/src/builtin/WeakRefObject.h new file mode 100644 index 0000000000..66acfca27e --- /dev/null +++ b/js/src/builtin/WeakRefObject.h @@ -0,0 +1,43 @@ +/* -*- 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 builtin_WeakRefObject_h +#define builtin_WeakRefObject_h + +#include "vm/NativeObject.h" + +namespace js { + +class WeakRefObject : public NativeObject { + public: + enum { TargetSlot, SlotCount }; + + static const JSClass class_; + static const JSClass protoClass_; + + JSObject* target() { return maybePtrFromReservedSlot<JSObject>(TargetSlot); } + + void setTargetUnbarriered(JSObject* target); + void clearTarget(); + + private: + static const JSClassOps classOps_; + static const ClassSpec classSpec_; + static const JSPropertySpec properties[]; + static const JSFunctionSpec methods[]; + + [[nodiscard]] static bool construct(JSContext* cx, unsigned argc, Value* vp); + static void trace(JSTracer* trc, JSObject* obj); + static void finalize(JS::GCContext* gcx, JSObject* obj); + + static bool preserveDOMWrapper(JSContext* cx, HandleObject obj); + + static bool deref(JSContext* cx, unsigned argc, Value* vp); + static void readBarrier(JSContext* cx, Handle<WeakRefObject*> self); +}; + +} // namespace js +#endif /* builtin_WeakRefObject_h */ diff --git a/js/src/builtin/WeakSet.js b/js/src/builtin/WeakSet.js new file mode 100644 index 0000000000..adf067863a --- /dev/null +++ b/js/src/builtin/WeakSet.js @@ -0,0 +1,22 @@ +/* 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/. */ + +// ES2017 draft rev 0e10c9f29fca1385980c08a7d5e7bb3eb775e2e4 +// 23.4.1.1 WeakSet, steps 6-8 +function WeakSetConstructorInit(iterable) { + var set = this; + + // Step 6.a. + var adder = set.add; + + // Step 6.b. + if (!IsCallable(adder)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, typeof adder); + } + + // Steps 6.c-8. + for (var nextValue of allowContentIter(iterable)) { + callContentFunction(adder, set, nextValue); + } +} diff --git a/js/src/builtin/WeakSetObject.cpp b/js/src/builtin/WeakSetObject.cpp new file mode 100644 index 0000000000..fe71183d85 --- /dev/null +++ b/js/src/builtin/WeakSetObject.cpp @@ -0,0 +1,237 @@ +/* -*- 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 "builtin/WeakSetObject.h" + +#include "builtin/MapObject.h" +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "js/PropertySpec.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/SelfHosting.h" + +#include "builtin/WeakMapObject-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +/* static */ MOZ_ALWAYS_INLINE bool WeakSetObject::is(HandleValue v) { + return v.isObject() && v.toObject().is<WeakSetObject>(); +} + +// ES2018 draft rev 7a2d3f053ecc2336fc19f377c55d52d78b11b296 +// 23.4.3.1 WeakSet.prototype.add ( value ) +/* static */ MOZ_ALWAYS_INLINE bool WeakSetObject::add_impl( + JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + // Step 4. + if (!args.get(0).isObject()) { + ReportNotObject(cx, JSMSG_OBJECT_REQUIRED_WEAKSET_VAL, args.get(0)); + return false; + } + + // Steps 5-7. + RootedObject value(cx, &args[0].toObject()); + Rooted<WeakSetObject*> map(cx, &args.thisv().toObject().as<WeakSetObject>()); + if (!WeakCollectionPutEntryInternal(cx, map, value, TrueHandleValue)) { + return false; + } + + // Steps 6.a.i, 8. + args.rval().set(args.thisv()); + return true; +} + +/* static */ +bool WeakSetObject::add(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-3. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<WeakSetObject::is, WeakSetObject::add_impl>(cx, + args); +} + +// ES2018 draft rev 7a2d3f053ecc2336fc19f377c55d52d78b11b296 +// 23.4.3.3 WeakSet.prototype.delete ( value ) +/* static */ MOZ_ALWAYS_INLINE bool WeakSetObject::delete_impl( + JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + // Step 4. + if (!args.get(0).isObject()) { + args.rval().setBoolean(false); + return true; + } + + // Steps 5-6. + if (ObjectValueWeakMap* map = + args.thisv().toObject().as<WeakSetObject>().getMap()) { + JSObject* value = &args[0].toObject(); + if (ObjectValueWeakMap::Ptr ptr = map->lookup(value)) { + map->remove(ptr); + args.rval().setBoolean(true); + return true; + } + } + + // Step 7. + args.rval().setBoolean(false); + return true; +} + +/* static */ +bool WeakSetObject::delete_(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-3. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<WeakSetObject::is, WeakSetObject::delete_impl>( + cx, args); +} + +// ES2018 draft rev 7a2d3f053ecc2336fc19f377c55d52d78b11b296 +// 23.4.3.4 WeakSet.prototype.has ( value ) +/* static */ MOZ_ALWAYS_INLINE bool WeakSetObject::has_impl( + JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(is(args.thisv())); + + // Step 5. + if (!args.get(0).isObject()) { + args.rval().setBoolean(false); + return true; + } + + // Steps 4, 6. + if (ObjectValueWeakMap* map = + args.thisv().toObject().as<WeakSetObject>().getMap()) { + JSObject* value = &args[0].toObject(); + if (map->has(value)) { + args.rval().setBoolean(true); + return true; + } + } + + // Step 7. + args.rval().setBoolean(false); + return true; +} + +/* static */ +bool WeakSetObject::has(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-3. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<WeakSetObject::is, WeakSetObject::has_impl>(cx, + args); +} + +const ClassSpec WeakSetObject::classSpec_ = { + GenericCreateConstructor<WeakSetObject::construct, 0, + gc::AllocKind::FUNCTION>, + GenericCreatePrototype<WeakSetObject>, + nullptr, + nullptr, + WeakSetObject::methods, + WeakSetObject::properties, +}; + +const JSClass WeakSetObject::class_ = { + "WeakSet", + JSCLASS_HAS_RESERVED_SLOTS(SlotCount) | + JSCLASS_HAS_CACHED_PROTO(JSProto_WeakSet) | JSCLASS_BACKGROUND_FINALIZE, + &WeakCollectionObject::classOps_, &WeakSetObject::classSpec_}; + +const JSClass WeakSetObject::protoClass_ = { + "WeakSet.prototype", JSCLASS_HAS_CACHED_PROTO(JSProto_WeakSet), + JS_NULL_CLASS_OPS, &WeakSetObject::classSpec_}; + +const JSPropertySpec WeakSetObject::properties[] = { + JS_STRING_SYM_PS(toStringTag, "WeakSet", JSPROP_READONLY), JS_PS_END}; + +const JSFunctionSpec WeakSetObject::methods[] = { + JS_FN("add", add, 1, 0), JS_FN("delete", delete_, 1, 0), + JS_FN("has", has, 1, 0), JS_FS_END}; + +WeakSetObject* WeakSetObject::create(JSContext* cx, + HandleObject proto /* = nullptr */) { + return NewObjectWithClassProto<WeakSetObject>(cx, proto); +} + +bool WeakSetObject::isBuiltinAdd(HandleValue add) { + return IsNativeFunction(add, WeakSetObject::add); +} + +bool WeakSetObject::construct(JSContext* cx, unsigned argc, Value* vp) { + // Based on our "Set" implementation instead of the more general ES6 steps. + CallArgs args = CallArgsFromVp(argc, vp); + + if (!ThrowIfNotConstructing(cx, args, "WeakSet")) { + return false; + } + + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_WeakSet, &proto)) { + return false; + } + + Rooted<WeakSetObject*> obj(cx, WeakSetObject::create(cx, proto)); + if (!obj) { + return false; + } + + if (!args.get(0).isNullOrUndefined()) { + RootedValue iterable(cx, args[0]); + bool optimized = false; + if (!IsOptimizableInitForSet<GlobalObject::getOrCreateWeakSetPrototype, + isBuiltinAdd>(cx, obj, iterable, &optimized)) { + return false; + } + + if (optimized) { + RootedValue keyVal(cx); + RootedObject keyObject(cx); + Rooted<ArrayObject*> array(cx, &iterable.toObject().as<ArrayObject>()); + for (uint32_t index = 0; index < array->getDenseInitializedLength(); + ++index) { + keyVal.set(array->getDenseElement(index)); + MOZ_ASSERT(!keyVal.isMagic(JS_ELEMENTS_HOLE)); + + if (keyVal.isPrimitive()) { + ReportNotObject(cx, JSMSG_OBJECT_REQUIRED_WEAKSET_VAL, keyVal); + return false; + } + + keyObject = &keyVal.toObject(); + if (!WeakCollectionPutEntryInternal(cx, obj, keyObject, + TrueHandleValue)) { + return false; + } + } + } else { + FixedInvokeArgs<1> args2(cx); + args2[0].set(args[0]); + + RootedValue thisv(cx, ObjectValue(*obj)); + if (!CallSelfHostedFunction(cx, cx->names().WeakSetConstructorInit, thisv, + args2, args2.rval())) { + return false; + } + } + } + + args.rval().setObject(*obj); + return true; +} + +JS_PUBLIC_API bool JS_NondeterministicGetWeakSetKeys(JSContext* cx, + HandleObject objArg, + MutableHandleObject ret) { + RootedObject obj(cx, UncheckedUnwrap(objArg)); + if (!obj || !obj->is<WeakSetObject>()) { + ret.set(nullptr); + return true; + } + return WeakCollectionObject::nondeterministicGetKeys( + cx, obj.as<WeakCollectionObject>(), ret); +} diff --git a/js/src/builtin/WeakSetObject.h b/js/src/builtin/WeakSetObject.h new file mode 100644 index 0000000000..e374ff688b --- /dev/null +++ b/js/src/builtin/WeakSetObject.h @@ -0,0 +1,50 @@ +/* -*- 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 builtin_WeakSetObject_h +#define builtin_WeakSetObject_h + +#include "builtin/WeakMapObject.h" + +namespace js { + +class WeakSetObject : public WeakCollectionObject { + public: + static const JSClass class_; + static const JSClass protoClass_; + + private: + static const ClassSpec classSpec_; + + static const JSPropertySpec properties[]; + static const JSFunctionSpec methods[]; + + static WeakSetObject* create(JSContext* cx, HandleObject proto = nullptr); + [[nodiscard]] static bool construct(JSContext* cx, unsigned argc, Value* vp); + + [[nodiscard]] static MOZ_ALWAYS_INLINE bool is(HandleValue v); + + [[nodiscard]] static MOZ_ALWAYS_INLINE bool add_impl(JSContext* cx, + const CallArgs& args); + [[nodiscard]] static bool add(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static MOZ_ALWAYS_INLINE bool delete_impl(JSContext* cx, + const CallArgs& args); + [[nodiscard]] static bool delete_(JSContext* cx, unsigned argc, Value* vp); + [[nodiscard]] static MOZ_ALWAYS_INLINE bool has_impl(JSContext* cx, + const CallArgs& args); + [[nodiscard]] static bool has(JSContext* cx, unsigned argc, Value* vp); + + static bool isBuiltinAdd(HandleValue add); +}; + +} // namespace js + +template <> +inline bool JSObject::is<js::WeakCollectionObject>() const { + return is<js::WeakMapObject>() || is<js::WeakSetObject>(); +} + +#endif /* builtin_WeakSetObject_h */ diff --git a/js/src/builtin/WrappedFunctionObject.cpp b/js/src/builtin/WrappedFunctionObject.cpp new file mode 100644 index 0000000000..97a14a4e5f --- /dev/null +++ b/js/src/builtin/WrappedFunctionObject.cpp @@ -0,0 +1,339 @@ +/* -*- 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 "builtin/WrappedFunctionObject.h" + +#include <string_view> + +#include "jsapi.h" + +#include "builtin/ShadowRealm.h" +#include "js/CallAndConstruct.h" +#include "js/Class.h" +#include "js/ErrorReport.h" +#include "js/Exception.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "util/StringBuffer.h" +#include "vm/Compartment.h" +#include "vm/Interpreter.h" +#include "vm/JSFunction.h" +#include "vm/ObjectOperations.h" + +#include "vm/JSFunction-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/Realm-inl.h" + +using namespace js; +using namespace JS; + +// GetWrappedValue ( callerRealm: a Realm Record, value: unknown ) +bool js::GetWrappedValue(JSContext* cx, Realm* callerRealm, Handle<Value> value, + MutableHandle<Value> res) { + cx->check(value); + + // Step 2. Return value (Reordered) + if (!value.isObject()) { + res.set(value); + return true; + } + + // Step 1. If Type(value) is Object, then + // a. If IsCallable(value) is false, throw a TypeError exception. + Rooted<JSObject*> objectVal(cx, &value.toObject()); + if (!IsCallable(objectVal)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_SHADOW_REALM_INVALID_RETURN); + return false; + } + + // b. Return ? WrappedFunctionCreate(callerRealm, value). + return WrappedFunctionCreate(cx, callerRealm, objectVal, res); +} + +// [[Call]] +// https://tc39.es/proposal-shadowrealm/#sec-wrapped-function-exotic-objects-call-thisargument-argumentslist +// https://tc39.es/proposal-shadowrealm/#sec-ordinary-wrapped-function-call +static bool WrappedFunction_Call(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Rooted<JSObject*> callee(cx, &args.callee()); + MOZ_ASSERT(callee->is<WrappedFunctionObject>()); + + Handle<WrappedFunctionObject*> fun = callee.as<WrappedFunctionObject>(); + + // PrepareForWrappedFunctionCall is a no-op in our implementation, because + // we've already entered the correct realm. + MOZ_ASSERT(cx->realm() == fun->realm()); + + // The next steps refer to the OrdinaryWrappedFunctionCall operation. + + // 1. Let target be F.[[WrappedTargetFunction]]. + Rooted<JSObject*> target(cx, fun->getTargetFunction()); + + // 2. Assert: IsCallable(target) is true. + MOZ_ASSERT(IsCallable(ObjectValue(*target))); + + // 3. Let callerRealm be F.[[Realm]]. + Rooted<Realm*> callerRealm(cx, fun->realm()); + + // 4. NOTE: Any exception objects produced after this point are associated + // with callerRealm. + // + // Implicit in our implementation, because |callerRealm| is already the + // current realm. + + // 5. Let targetRealm be ? GetFunctionRealm(target). + Rooted<Realm*> targetRealm(cx, GetFunctionRealm(cx, target)); + if (!targetRealm) { + return false; + } + + // 6. Let wrappedArgs be a new empty List. + InvokeArgs wrappedArgs(cx); + if (!wrappedArgs.init(cx, args.length())) { + return false; + } + + // 7. For each element arg of argumentsList, do + // a. Let wrappedValue be ? GetWrappedValue(targetRealm, arg). + // b. Append wrappedValue to wrappedArgs. + Rooted<Value> element(cx); + for (size_t i = 0; i < args.length(); i++) { + element = args.get(i); + if (!GetWrappedValue(cx, targetRealm, element, &element)) { + return false; + } + + wrappedArgs[i].set(element); + } + + // 8. Let wrappedThisArgument to ? GetWrappedValue(targetRealm, + // thisArgument). + Rooted<Value> wrappedThisArgument(cx); + if (!GetWrappedValue(cx, targetRealm, args.thisv(), &wrappedThisArgument)) { + return false; + } + + // 9. Let result be the Completion Record of Call(target, + // wrappedThisArgument, wrappedArgs). + Rooted<Value> targetValue(cx, ObjectValue(*target)); + Rooted<Value> result(cx); + if (!js::Call(cx, targetValue, wrappedThisArgument, wrappedArgs, &result)) { + // 11. Else (reordered); + // a. Throw a TypeError exception. + ReportPotentiallyDetailedMessage( + cx, JSMSG_SHADOW_REALM_WRAPPED_EXECUTION_FAILURE_DETAIL, + JSMSG_SHADOW_REALM_WRAPPED_EXECUTION_FAILURE); + return false; + } + + // 10. If result.[[Type]] is normal or result.[[Type]] is return, then + // a. Return ? GetWrappedValue(callerRealm, result.[[Value]]). + if (!GetWrappedValue(cx, callerRealm, result, args.rval())) { + return false; + } + + return true; +} + +static bool CopyNameAndLength(JSContext* cx, HandleObject fun, + HandleObject target) { + // 1. If argCount is undefined, then set argCount to 0 (implicit) + constexpr int32_t argCount = 0; + + // 2. Let L be 0. + double length = 0; + + // 3. Let targetHasLength be ? HasOwnProperty(Target, "length"). + // + // Try to avoid invoking the resolve hook. + // Also see ComputeLengthValue in BoundFunctionObject.cpp. + if (target->is<JSFunction>() && + !target->as<JSFunction>().hasResolvedLength()) { + uint16_t targetLen; + if (!JSFunction::getUnresolvedLength(cx, target.as<JSFunction>(), + &targetLen)) { + return false; + } + + length = std::max(0.0, double(targetLen) - argCount); + } else { + Rooted<jsid> lengthId(cx, NameToId(cx->names().length)); + + bool targetHasLength; + if (!HasOwnProperty(cx, target, lengthId, &targetHasLength)) { + return false; + } + + // 4. If targetHasLength is true, then + if (targetHasLength) { + // a. Let targetLen be ? Get(Target, "length"). + Rooted<Value> targetLen(cx); + if (!GetProperty(cx, target, target, lengthId, &targetLen)) { + return false; + } + + // b. If Type(targetLen) is Number, then + // i. If targetLen is +∞𝔽, set L to +∞. + // ii. Else if targetLen is -∞𝔽, set L to 0. + // iii. Else, + // 1. Let targetLenAsInt be ! ToIntegerOrInfinity(targetLen). + // 2. Assert: targetLenAsInt is finite. + // 3. Set L to max(targetLenAsInt - argCount, 0). + if (targetLen.isNumber()) { + length = std::max(0.0, JS::ToInteger(targetLen.toNumber()) - argCount); + } + } + } + + // 5. Perform ! SetFunctionLength(F, L). + Rooted<Value> rootedLength(cx, NumberValue(length)); + if (!DefineDataProperty(cx, fun, cx->names().length, rootedLength, + JSPROP_READONLY)) { + return false; + } + + // 6. Let targetName be ? Get(Target, "name"). + // + // Try to avoid invoking the resolve hook. + Rooted<Value> targetName(cx); + if (target->is<JSFunction>() && !target->as<JSFunction>().hasResolvedName()) { + JSFunction* targetFun = &target->as<JSFunction>(); + targetName.setString(targetFun->infallibleGetUnresolvedName(cx)); + } else { + if (!GetProperty(cx, target, target, cx->names().name, &targetName)) { + return false; + } + } + + // 7. If Type(targetName) is not String, set targetName to the empty String. + if (!targetName.isString()) { + targetName = StringValue(cx->runtime()->emptyString); + } + + // 8. Perform ! SetFunctionName(F, targetName, prefix). + return DefineDataProperty(cx, fun, cx->names().name, targetName, + JSPROP_READONLY); +} + +static JSString* ToStringOp(JSContext* cx, JS::HandleObject obj, + bool isToSource) { + // Return an unnamed native function to match the behavior of bound + // functions. + // + // NOTE: The current value of the "name" property can be any value, it's not + // necessarily a string value. It can also be an accessor property which could + // lead to executing side-effects, which isn't allowed per the spec, cf. + // <https://tc39.es/ecma262/#sec-function.prototype.tostring>. Even if it's a + // data property with a string value, we'd still need to validate the string + // can be parsed as a |PropertyName| production before using it as part of the + // output. + constexpr std::string_view nativeCode = "function () {\n [native code]\n}"; + + return NewStringCopy<CanGC>(cx, nativeCode); +} + +static const JSClassOps classOps = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + nullptr, // finalize + WrappedFunction_Call, // call + nullptr, // construct + nullptr, // trace +}; + +static const ObjectOps objOps = { + nullptr, // lookupProperty + nullptr, // defineProperty + nullptr, // hasProperty + nullptr, // getProperty + nullptr, // setProperty + nullptr, // getOwnPropertyDescriptor + nullptr, // deleteProperty + nullptr, // getElements + ToStringOp, // funToString +}; + +const JSClass WrappedFunctionObject::class_ = { + "WrappedFunctionObject", + JSCLASS_HAS_CACHED_PROTO( + JSProto_Function) | // This sets the prototype to Function.prototype, + // Step 3 of WrappedFunctionCreate + JSCLASS_HAS_RESERVED_SLOTS(WrappedFunctionObject::SlotCount), + &classOps, + JS_NULL_CLASS_SPEC, + JS_NULL_CLASS_EXT, + &objOps, +}; + +// WrappedFunctionCreate ( callerRealm: a Realm Record, Target: a function +// object) +bool js::WrappedFunctionCreate(JSContext* cx, Realm* callerRealm, + HandleObject target, MutableHandle<Value> res) { + cx->check(target); + + WrappedFunctionObject* wrapped = nullptr; + { + // Ensure that the function object has the correct realm by allocating it + // into that realm. + Rooted<JSObject*> global(cx, callerRealm->maybeGlobal()); + MOZ_RELEASE_ASSERT( + global, "global is null; executing in a realm that's being GC'd?"); + AutoRealm ar(cx, global); + + MOZ_ASSERT(target); + + // Target *could* be a function from another compartment. + Rooted<JSObject*> maybeWrappedTarget(cx, target); + if (!cx->compartment()->wrap(cx, &maybeWrappedTarget)) { + return false; + } + + // 1. Let internalSlotsList be the internal slots listed in Table 2, plus + // [[Prototype]] and [[Extensible]]. + // 2. Let wrapped be ! MakeBasicObject(internalSlotsList). + // 3. Set wrapped.[[Prototype]] to + // callerRealm.[[Intrinsics]].[[%Function.prototype%]]. + wrapped = NewBuiltinClassInstance<WrappedFunctionObject>(cx); + if (!wrapped) { + return false; + } + + // 4. Set wrapped.[[Call]] as described in 2.1 (implicit in JSClass call + // hook) + // 5. Set wrapped.[[WrappedTargetFunction]] to Target. + wrapped->setTargetFunction(*maybeWrappedTarget); + // 6. Set wrapped.[[Realm]] to callerRealm. (implicitly the realm of + // wrapped, which we assured with the AutoRealm + + MOZ_ASSERT(wrapped->realm() == callerRealm); + } + + // Wrap |wrapped| to the current compartment. + RootedObject obj(cx, wrapped); + if (!cx->compartment()->wrap(cx, &obj)) { + return false; + } + + // 7. Let result be CopyNameAndLength(wrapped, Target). + if (!CopyNameAndLength(cx, obj, target)) { + // 8. If result is an Abrupt Completion, throw a TypeError exception. + cx->clearPendingException(); + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_SHADOW_REALM_WRAP_FAILURE); + return false; + } + + // 9. Return wrapped. + res.set(ObjectValue(*obj)); + return true; +} diff --git a/js/src/builtin/WrappedFunctionObject.h b/js/src/builtin/WrappedFunctionObject.h new file mode 100644 index 0000000000..076f4f2b4d --- /dev/null +++ b/js/src/builtin/WrappedFunctionObject.h @@ -0,0 +1,43 @@ +/* -*- 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 builtin_WrappedFunctionObject_h +#define builtin_WrappedFunctionObject_h + +#include "js/Value.h" +#include "vm/NativeObject.h" + +namespace js { + +// Implementing Wrapped Function Exotic Objects from the ShadowRealms proposal +// https://tc39.es/proposal-shadowrealm/#sec-wrapped-function-exotic-objects +// +// These are produced as callables are passed across ShadowRealm boundaries, +// preventing functions from piercing the shadow realm barrier. +class WrappedFunctionObject : public NativeObject { + public: + static const JSClass class_; + + enum { WrappedTargetFunctionSlot, SlotCount }; + + JSObject* getTargetFunction() const { + return &getFixedSlot(WrappedTargetFunctionSlot).toObject(); + } + + void setTargetFunction(JSObject& obj) { + setFixedSlot(WrappedTargetFunctionSlot, ObjectValue(obj)); + } +}; + +bool WrappedFunctionCreate(JSContext* cx, Realm* callerRealm, + Handle<JSObject*> target, MutableHandle<Value> res); + +bool GetWrappedValue(JSContext* cx, Realm* callerRealm, Handle<Value> value, + MutableHandle<Value> res); + +} // namespace js + +#endif diff --git a/js/src/builtin/embedjs.py b/js/src/builtin/embedjs.py new file mode 100644 index 0000000000..a82aaab951 --- /dev/null +++ b/js/src/builtin/embedjs.py @@ -0,0 +1,204 @@ +# 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/. +# +# ToCAsciiArray and ToCArray are from V8's js2c.py. +# +# Copyright 2012 the V8 project authors. All rights reserved. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# This utility converts JS files containing self-hosted builtins into a C +# header file that can be embedded into SpiderMonkey. +# +# It uses the C preprocessor to process its inputs. + +import errno +import os +import re +import shlex +import subprocess +import sys + +import buildconfig +import mozpack.path as mozpath +from mozfile import which + + +def ToCAsciiArray(lines): + result = [] + for chr in lines: + value = ord(chr) + assert value < 128 + result.append(str(value)) + return ", ".join(result) + + +def ToCArray(lines): + result = [] + for chr in lines: + result.append(str(chr)) + return ", ".join(result) + + +HEADER_TEMPLATE = """\ +/* 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/. */ + +namespace js { +namespace %(namespace)s { + static const %(sources_type)s data[] = { %(sources_data)s }; + + static const %(sources_type)s * const %(sources_name)s = reinterpret_cast<const %(sources_type)s *>(data); + + uint32_t GetCompressedSize() { + return %(compressed_total_length)i; + } + + uint32_t GetRawScriptsSize() { + return %(raw_total_length)i; + } +} // selfhosted +} // js +""" # NOQA: E501 + + +def embed( + cxx, preprocessorOption, cppflags, msgs, sources, c_out, js_out, namespace, env +): + objdir = os.getcwd() + # Use relative pathnames to avoid path translation issues in WSL. + combinedSources = "\n".join( + [msgs] + + [ + '#include "%(s)s"' % {"s": mozpath.relpath(source, objdir)} + for source in sources + ] + ) + args = cppflags + ["-D%(k)s=%(v)s" % {"k": k, "v": env[k]} for k in env] + preprocessed = preprocess(cxx, preprocessorOption, combinedSources, args) + processed = "\n".join( + [ + line + for line in preprocessed.splitlines() + if (line.strip() and not line.startswith("#")) + ] + ) + + js_out.write(processed) + import zlib + + compressed = zlib.compress(processed.encode("utf-8")) + data = ToCArray(compressed) + c_out.write( + HEADER_TEMPLATE + % { + "sources_type": "unsigned char", + "sources_data": data, + "sources_name": "compressedSources", + "compressed_total_length": len(compressed), + "raw_total_length": len(processed), + "namespace": namespace, + } + ) + + +def preprocess(cxx, preprocessorOption, source, args=[]): + if not os.path.exists(cxx[0]): + binary = cxx[0] + cxx[0] = which(binary) + if not cxx[0]: + raise OSError(errno.ENOENT, "%s not found on PATH" % binary) + + # Clang seems to complain and not output anything if the extension of the + # input is not something it recognizes, so just fake a .cpp here. + tmpIn = "self-hosting-cpp-input.cpp" + tmpOut = "self-hosting-preprocessed.pp" + outputArg = shlex.split(preprocessorOption + tmpOut) + + with open(tmpIn, "wb") as input: + input.write(source.encode("utf-8")) + print(" ".join(cxx + outputArg + args + [tmpIn])) + result = subprocess.Popen(cxx + outputArg + args + [tmpIn]).wait() + if result != 0: + sys.exit(result) + with open(tmpOut, "r") as output: + processed = output.read() + os.remove(tmpIn) + os.remove(tmpOut) + return processed + + +def messages(jsmsg): + defines = [] + for line in open(jsmsg): + match = re.match("MSG_DEF\((JSMSG_(\w+))", line) + if match: + defines.append("#define %s %i" % (match.group(1), len(defines))) + continue + + # Make sure that MSG_DEF isn't preceded by whitespace + assert not line.strip().startswith("MSG_DEF") + + # This script doesn't support preprocessor + assert not line.strip().startswith("#") + return "\n".join(defines) + + +def get_config_defines(buildconfig): + # Collect defines equivalent to ACDEFINES and add MOZ_DEBUG_DEFINES. + env = buildconfig.defines["ALLDEFINES"] + for define in buildconfig.substs["MOZ_DEBUG_DEFINES"]: + env[define] = 1 + return env + + +def process_inputs(namespace, c_out, msg_file, inputs): + deps = [path for path in inputs if path.endswith(".h") or path.endswith(".h.js")] + sources = [ + path for path in inputs if path.endswith(".js") and not path.endswith(".h.js") + ] + assert len(deps) + len(sources) == len(inputs) + cxx = shlex.split(buildconfig.substs["CXX"]) + pp_option = buildconfig.substs["PREPROCESS_OPTION"] + cppflags = buildconfig.substs["OS_CPPFLAGS"] + cppflags += shlex.split(buildconfig.substs["WARNINGS_AS_ERRORS"]) + env = get_config_defines(buildconfig) + js_path = re.sub(r"\.out\.h$", "", c_out.name) + ".js" + msgs = messages(msg_file) + with open(js_path, "w") as js_out: + embed(cxx, pp_option, cppflags, msgs, sources, c_out, js_out, namespace, env) + + +def generate_selfhosted(c_out, msg_file, *inputs): + # Called from moz.build to embed selfhosted JS. + process_inputs("selfhosted", c_out, msg_file, inputs) + + +def generate_shellmoduleloader(c_out, msg_file, *inputs): + # Called from moz.build to embed shell module loader JS. + process_inputs("moduleloader", c_out, msg_file, inputs) diff --git a/js/src/builtin/intl/Collator.cpp b/js/src/builtin/intl/Collator.cpp new file mode 100644 index 0000000000..449de1dc45 --- /dev/null +++ b/js/src/builtin/intl/Collator.cpp @@ -0,0 +1,472 @@ +/* -*- 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/. */ + +/* Intl.Collator implementation. */ + +#include "builtin/intl/Collator.h" + +#include "mozilla/Assertions.h" +#include "mozilla/intl/Collator.h" +#include "mozilla/intl/Locale.h" +#include "mozilla/Span.h" + +#include "builtin/Array.h" +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/FormatBuffer.h" +#include "builtin/intl/LanguageTag.h" +#include "builtin/intl/SharedIntlData.h" +#include "gc/GCContext.h" +#include "js/PropertySpec.h" +#include "js/StableStringChars.h" +#include "js/TypeDecls.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/Runtime.h" +#include "vm/StringType.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/GeckoProfiler-inl.h" +#include "vm/JSObject-inl.h" + +using namespace js; + +using JS::AutoStableStringChars; + +using js::intl::ReportInternalError; +using js::intl::SharedIntlData; + +const JSClassOps CollatorObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + CollatorObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass CollatorObject::class_ = { + "Intl.Collator", + JSCLASS_HAS_RESERVED_SLOTS(CollatorObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_Collator) | + JSCLASS_FOREGROUND_FINALIZE, + &CollatorObject::classOps_, &CollatorObject::classSpec_}; + +const JSClass& CollatorObject::protoClass_ = PlainObject::class_; + +static bool collator_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().Collator); + return true; +} + +static const JSFunctionSpec collator_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_Collator_supportedLocalesOf", + 1, 0), + JS_FS_END}; + +static const JSFunctionSpec collator_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_Collator_resolvedOptions", 0, 0), + JS_FN(js_toSource_str, collator_toSource, 0, 0), JS_FS_END}; + +static const JSPropertySpec collator_properties[] = { + JS_SELF_HOSTED_GET("compare", "$Intl_Collator_compare_get", 0), + JS_STRING_SYM_PS(toStringTag, "Intl.Collator", JSPROP_READONLY), JS_PS_END}; + +static bool Collator(JSContext* cx, unsigned argc, Value* vp); + +const ClassSpec CollatorObject::classSpec_ = { + GenericCreateConstructor<Collator, 0, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<CollatorObject>, + collator_static_methods, + nullptr, + collator_methods, + collator_properties, + nullptr, + ClassSpec::DontDefineConstructor}; + +/** + * 10.1.2 Intl.Collator([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool Collator(JSContext* cx, const CallArgs& args) { + AutoJSConstructorProfilerEntry pseudoFrame(cx, "Intl.Collator"); + + // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). + + // Steps 2-5 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Collator, &proto)) { + return false; + } + + Rooted<CollatorObject*> collator( + cx, NewObjectWithClassProto<CollatorObject>(cx, proto)); + if (!collator) { + return false; + } + + HandleValue locales = args.get(0); + HandleValue options = args.get(1); + + // Step 6. + if (!intl::InitializeObject(cx, collator, cx->names().InitializeCollator, + locales, options)) { + return false; + } + + args.rval().setObject(*collator); + return true; +} + +static bool Collator(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return Collator(cx, args); +} + +bool js::intl_Collator(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + + return Collator(cx, args); +} + +void js::CollatorObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + + if (mozilla::intl::Collator* coll = obj->as<CollatorObject>().getCollator()) { + intl::RemoveICUCellMemory(gcx, obj, CollatorObject::EstimatedMemoryUse); + delete coll; + } +} + +bool js::intl_availableCollations(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + UniqueChars locale = intl::EncodeLocale(cx, args[0].toString()); + if (!locale) { + return false; + } + auto keywords = + mozilla::intl::Collator::GetBcp47KeywordValuesForLocale(locale.get()); + if (keywords.isErr()) { + ReportInternalError(cx, keywords.unwrapErr()); + return false; + } + + RootedObject collations(cx, NewDenseEmptyArray(cx)); + if (!collations) { + return false; + } + + // The first element of the collations array must be |null| per + // ES2017 Intl, 10.2.3 Internal Slots. + if (!NewbornArrayPush(cx, collations, NullValue())) { + return false; + } + + for (auto result : keywords.unwrap()) { + if (result.isErr()) { + ReportInternalError(cx); + return false; + } + mozilla::Span<const char> collation = result.unwrap(); + + // Per ECMA-402, 10.2.3, we don't include standard and search: + // "The values 'standard' and 'search' must not be used as elements in + // any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co + // array." + static constexpr auto standard = mozilla::MakeStringSpan("standard"); + static constexpr auto search = mozilla::MakeStringSpan("search"); + if (collation == standard || collation == search) { + continue; + } + + JSString* jscollation = NewStringCopy<CanGC>(cx, collation); + if (!jscollation) { + return false; + } + if (!NewbornArrayPush(cx, collations, StringValue(jscollation))) { + return false; + } + } + + args.rval().setObject(*collations); + return true; +} + +/** + * Returns a new mozilla::intl::Collator with the locale and collation options + * of the given Collator. + */ +static mozilla::intl::Collator* NewIntlCollator( + JSContext* cx, Handle<CollatorObject*> collator) { + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, collator)); + if (!internals) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) { + return nullptr; + } + + mozilla::intl::Locale tag; + { + Rooted<JSLinearString*> locale(cx, value.toString()->ensureLinear(cx)); + if (!locale) { + return nullptr; + } + + if (!intl::ParseLocale(cx, locale, tag)) { + return nullptr; + } + } + + using mozilla::intl::Collator; + + Collator::Options options{}; + + if (!GetProperty(cx, internals, internals, cx->names().usage, &value)) { + return nullptr; + } + + enum class Usage { Search, Sort }; + + Usage usage; + { + JSLinearString* str = value.toString()->ensureLinear(cx); + if (!str) { + return nullptr; + } + + if (StringEqualsLiteral(str, "search")) { + usage = Usage::Search; + } else { + MOZ_ASSERT(StringEqualsLiteral(str, "sort")); + usage = Usage::Sort; + } + } + + JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx); + + // ICU expects collation as Unicode locale extensions on locale. + if (usage == Usage::Search) { + if (!keywords.emplaceBack("co", cx->names().search)) { + return nullptr; + } + + // Search collations can't select a different collation, so the collation + // property is guaranteed to be "default". +#ifdef DEBUG + if (!GetProperty(cx, internals, internals, cx->names().collation, &value)) { + return nullptr; + } + + JSLinearString* collation = value.toString()->ensureLinear(cx); + if (!collation) { + return nullptr; + } + + MOZ_ASSERT(StringEqualsLiteral(collation, "default")); +#endif + } else { + if (!GetProperty(cx, internals, internals, cx->names().collation, &value)) { + return nullptr; + } + + JSLinearString* collation = value.toString()->ensureLinear(cx); + if (!collation) { + return nullptr; + } + + // Set collation as a Unicode locale extension when it was specified. + if (!StringEqualsLiteral(collation, "default")) { + if (!keywords.emplaceBack("co", collation)) { + return nullptr; + } + } + } + + // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of the + // Unicode extension subtag. We're then relying on ICU to follow RFC 6067, + // which states that any trailing keywords using the same key should be + // ignored. + if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) { + return nullptr; + } + + intl::FormatBuffer<char> buffer(cx); + if (auto result = tag.ToString(buffer); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + + UniqueChars locale = buffer.extractStringZ(); + if (!locale) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().sensitivity, &value)) { + return nullptr; + } + + { + JSLinearString* sensitivity = value.toString()->ensureLinear(cx); + if (!sensitivity) { + return nullptr; + } + if (StringEqualsLiteral(sensitivity, "base")) { + options.sensitivity = Collator::Sensitivity::Base; + } else if (StringEqualsLiteral(sensitivity, "accent")) { + options.sensitivity = Collator::Sensitivity::Accent; + } else if (StringEqualsLiteral(sensitivity, "case")) { + options.sensitivity = Collator::Sensitivity::Case; + } else { + MOZ_ASSERT(StringEqualsLiteral(sensitivity, "variant")); + options.sensitivity = Collator::Sensitivity::Variant; + } + } + + if (!GetProperty(cx, internals, internals, cx->names().ignorePunctuation, + &value)) { + return nullptr; + } + options.ignorePunctuation = value.toBoolean(); + + if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) { + return nullptr; + } + if (!value.isUndefined()) { + options.numeric = value.toBoolean(); + } + + if (!GetProperty(cx, internals, internals, cx->names().caseFirst, &value)) { + return nullptr; + } + if (!value.isUndefined()) { + JSLinearString* caseFirst = value.toString()->ensureLinear(cx); + if (!caseFirst) { + return nullptr; + } + if (StringEqualsLiteral(caseFirst, "upper")) { + options.caseFirst = Collator::CaseFirst::Upper; + } else if (StringEqualsLiteral(caseFirst, "lower")) { + options.caseFirst = Collator::CaseFirst::Lower; + } else { + MOZ_ASSERT(StringEqualsLiteral(caseFirst, "false")); + options.caseFirst = Collator::CaseFirst::False; + } + } + + auto collResult = Collator::TryCreate(locale.get()); + if (collResult.isErr()) { + ReportInternalError(cx, collResult.unwrapErr()); + return nullptr; + } + auto coll = collResult.unwrap(); + + auto optResult = coll->SetOptions(options); + if (optResult.isErr()) { + ReportInternalError(cx, optResult.unwrapErr()); + return nullptr; + } + + return coll.release(); +} + +static mozilla::intl::Collator* GetOrCreateCollator( + JSContext* cx, Handle<CollatorObject*> collator) { + // Obtain a cached mozilla::intl::Collator object. + mozilla::intl::Collator* coll = collator->getCollator(); + if (coll) { + return coll; + } + + coll = NewIntlCollator(cx, collator); + if (!coll) { + return nullptr; + } + collator->setCollator(coll); + + intl::AddICUCellMemory(collator, CollatorObject::EstimatedMemoryUse); + return coll; +} + +static bool intl_CompareStrings(JSContext* cx, mozilla::intl::Collator* coll, + HandleString str1, HandleString str2, + MutableHandleValue result) { + MOZ_ASSERT(str1); + MOZ_ASSERT(str2); + + if (str1 == str2) { + result.setInt32(0); + return true; + } + + AutoStableStringChars stableChars1(cx); + if (!stableChars1.initTwoByte(cx, str1)) { + return false; + } + + AutoStableStringChars stableChars2(cx); + if (!stableChars2.initTwoByte(cx, str2)) { + return false; + } + + mozilla::Range<const char16_t> chars1 = stableChars1.twoByteRange(); + mozilla::Range<const char16_t> chars2 = stableChars2.twoByteRange(); + + result.setInt32(coll->CompareStrings(chars1, chars2)); + return true; +} + +bool js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(args[1].isString()); + MOZ_ASSERT(args[2].isString()); + + Rooted<CollatorObject*> collator(cx, + &args[0].toObject().as<CollatorObject>()); + + mozilla::intl::Collator* coll = GetOrCreateCollator(cx, collator); + if (!coll) { + return false; + } + + // Use the UCollator to actually compare the strings. + RootedString str1(cx, args[1].toString()); + RootedString str2(cx, args[2].toString()); + return intl_CompareStrings(cx, coll, str1, str2, args.rval()); +} + +bool js::intl_isUpperCaseFirst(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + + RootedString locale(cx, args[0].toString()); + bool isUpperFirst; + if (!sharedIntlData.isUpperCaseFirst(cx, locale, &isUpperFirst)) { + return false; + } + + args.rval().setBoolean(isUpperFirst); + return true; +} diff --git a/js/src/builtin/intl/Collator.h b/js/src/builtin/intl/Collator.h new file mode 100644 index 0000000000..764c838ce8 --- /dev/null +++ b/js/src/builtin/intl/Collator.h @@ -0,0 +1,104 @@ +/* -*- 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 builtin_intl_Collator_h +#define builtin_intl_Collator_h + +#include <stdint.h> + +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "vm/NativeObject.h" + +namespace mozilla::intl { +class Collator; +} + +namespace js { + +/******************** Collator ********************/ + +class CollatorObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t INTL_COLLATOR_SLOT = 1; + static constexpr uint32_t SLOT_COUNT = 2; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals " + "object slot"); + + // Estimated memory use for UCollator (see IcuMemoryUsage). + static constexpr size_t EstimatedMemoryUse = 1128; + + mozilla::intl::Collator* getCollator() const { + const auto& slot = getFixedSlot(INTL_COLLATOR_SLOT); + if (slot.isUndefined()) { + return nullptr; + } + return static_cast<mozilla::intl::Collator*>(slot.toPrivate()); + } + + void setCollator(mozilla::intl::Collator* collator) { + setFixedSlot(INTL_COLLATOR_SLOT, PrivateValue(collator)); + } + + private: + static const JSClassOps classOps_; + static const ClassSpec classSpec_; + + static void finalize(JS::GCContext* gcx, JSObject* obj); +}; + +/** + * Returns a new instance of the standard built-in Collator constructor. + * Self-hosted code cannot cache this constructor (as it does for others in + * Utilities.js) because it is initialized after self-hosted code is compiled. + * + * Usage: collator = intl_Collator(locales, options) + */ +[[nodiscard]] extern bool intl_Collator(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * Returns an array with the collation type identifiers per Unicode + * Technical Standard 35, Unicode Locale Data Markup Language, for the + * collations supported for the given locale. "standard" and "search" are + * excluded. + * + * Usage: collations = intl_availableCollations(locale) + */ +[[nodiscard]] extern bool intl_availableCollations(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * Compares x and y (which must be String values), and returns a number less + * than 0 if x < y, 0 if x = y, or a number greater than 0 if x > y according + * to the sort order for the locale and collation options of the given + * Collator. + * + * Spec: ECMAScript Internationalization API Specification, 10.3.2. + * + * Usage: result = intl_CompareStrings(collator, x, y) + */ +[[nodiscard]] extern bool intl_CompareStrings(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * Returns true if the given locale sorts upper-case before lower-case + * characters. + * + * Usage: result = intl_isUpperCaseFirst(locale) + */ +[[nodiscard]] extern bool intl_isUpperCaseFirst(JSContext* cx, unsigned argc, + JS::Value* vp); + +} // namespace js + +#endif /* builtin_intl_Collator_h */ diff --git a/js/src/builtin/intl/Collator.js b/js/src/builtin/intl/Collator.js new file mode 100644 index 0000000000..d780e3ce77 --- /dev/null +++ b/js/src/builtin/intl/Collator.js @@ -0,0 +1,468 @@ +/* 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/. */ + +/* Portions Copyright Norbert Lindenberg 2011-2012. */ + +/** + * Compute an internal properties object from |lazyCollatorData|. + */ +function resolveCollatorInternals(lazyCollatorData) { + assert(IsObject(lazyCollatorData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + var Collator = collatorInternalProperties; + + // Step 5. + internalProps.usage = lazyCollatorData.usage; + + // Steps 6-7. + var collatorIsSorting = lazyCollatorData.usage === "sort"; + var localeData = collatorIsSorting + ? Collator.sortLocaleData + : Collator.searchLocaleData; + + // Compute effective locale. + // Step 16. + var relevantExtensionKeys = Collator.relevantExtensionKeys; + + // Step 17. + var r = ResolveLocale( + "Collator", + lazyCollatorData.requestedLocales, + lazyCollatorData.opt, + relevantExtensionKeys, + localeData + ); + + // Step 18. + internalProps.locale = r.locale; + + // Step 19. + var collation = r.co; + + // Step 20. + if (collation === null) { + collation = "default"; + } + + // Step 21. + internalProps.collation = collation; + + // Step 22. + internalProps.numeric = r.kn === "true"; + + // Step 23. + internalProps.caseFirst = r.kf; + + // Compute remaining collation options. + // Step 25. + var s = lazyCollatorData.rawSensitivity; + if (s === undefined) { + // In theory the default sensitivity for the "search" collator is + // locale dependent; in reality the CLDR/ICU default strength is + // always tertiary. Therefore use "variant" as the default value for + // both collation modes. + s = "variant"; + } + + // Step 26. + internalProps.sensitivity = s; + + // Step 28. + internalProps.ignorePunctuation = lazyCollatorData.ignorePunctuation; + + // The caller is responsible for associating |internalProps| with the right + // object using |setInternalProperties|. + return internalProps; +} + +/** + * Returns an object containing the Collator internal properties of |obj|. + */ +function getCollatorInternals(obj) { + assert(IsObject(obj), "getCollatorInternals called with non-object"); + assert( + intl_GuardToCollator(obj) !== null, + "getCollatorInternals called with non-Collator" + ); + + var internals = getIntlObjectInternals(obj); + assert( + internals.type === "Collator", + "bad type escaped getIntlObjectInternals" + ); + + // If internal properties have already been computed, use them. + var internalProps = maybeInternalProperties(internals); + if (internalProps) { + return internalProps; + } + + // Otherwise it's time to fully create them. + internalProps = resolveCollatorInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * Initializes an object as a Collator. + * + * This method is complicated a moderate bit by its implementing initialization + * as a *lazy* concept. Everything that must happen now, does -- but we defer + * all the work we can until the object is actually used as a Collator. This + * later work occurs in |resolveCollatorInternals|; steps not noted here occur + * there. + * + * Spec: ECMAScript Internationalization API Specification, 10.1.1. + */ +function InitializeCollator(collator, locales, options) { + assert(IsObject(collator), "InitializeCollator called with non-object"); + assert( + intl_GuardToCollator(collator) !== null, + "InitializeCollator called with non-Collator" + ); + + // Lazy Collator data has the following structure: + // + // { + // requestedLocales: List of locales, + // usage: "sort" / "search", + // opt: // opt object computed in InitializeCollator + // { + // localeMatcher: "lookup" / "best fit", + // co: string matching a Unicode extension type / undefined + // kn: true / false / undefined, + // kf: "upper" / "lower" / "false" / undefined + // } + // rawSensitivity: "base" / "accent" / "case" / "variant" / undefined, + // ignorePunctuation: true / false + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every Collator lazy data object has *all* these properties, never a + // subset of them. + var lazyCollatorData = std_Object_create(null); + + // Step 1. + var requestedLocales = CanonicalizeLocaleList(locales); + lazyCollatorData.requestedLocales = requestedLocales; + + // Steps 2-3. + // + // If we ever need more speed here at startup, we should try to detect the + // case where |options === undefined| and then directly use the default + // value for each option. For now, just keep it simple. + if (options === undefined) { + options = std_Object_create(null); + } else { + options = ToObject(options); + } + + // Compute options that impact interpretation of locale. + // Step 4. + var u = GetOption(options, "usage", "string", ["sort", "search"], "sort"); + lazyCollatorData.usage = u; + + // Step 8. + var opt = new_Record(); + lazyCollatorData.opt = opt; + + // Steps 9-10. + var matcher = GetOption( + options, + "localeMatcher", + "string", + ["lookup", "best fit"], + "best fit" + ); + opt.localeMatcher = matcher; + + // https://github.com/tc39/ecma402/pull/459 + var collation = GetOption( + options, + "collation", + "string", + undefined, + undefined + ); + if (collation !== undefined) { + collation = intl_ValidateAndCanonicalizeUnicodeExtensionType( + collation, + "collation", + "co" + ); + } + opt.co = collation; + + // Steps 11-13. + var numericValue = GetOption( + options, + "numeric", + "boolean", + undefined, + undefined + ); + if (numericValue !== undefined) { + numericValue = numericValue ? "true" : "false"; + } + opt.kn = numericValue; + + // Steps 14-15. + var caseFirstValue = GetOption( + options, + "caseFirst", + "string", + ["upper", "lower", "false"], + undefined + ); + opt.kf = caseFirstValue; + + // Compute remaining collation options. + // Step 24. + var s = GetOption( + options, + "sensitivity", + "string", + ["base", "accent", "case", "variant"], + undefined + ); + lazyCollatorData.rawSensitivity = s; + + // Step 27. + var ip = GetOption(options, "ignorePunctuation", "boolean", undefined, false); + lazyCollatorData.ignorePunctuation = ip; + + // Step 29. + // + // We've done everything that must be done now: mark the lazy data as fully + // computed and install it. + initializeIntlObject(collator, "Collator", lazyCollatorData); +} + +/** + * Returns the subset of the given locale list for which this locale list has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript Internationalization API Specification, 10.2.2. + */ +function Intl_Collator_supportedLocalesOf(locales /*, options*/) { + var options = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 1. + var availableLocales = "Collator"; + + // Step 2. + var requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. + return SupportedLocales(availableLocales, requestedLocales, options); +} + +/** + * Collator internal properties. + * + * Spec: ECMAScript Internationalization API Specification, 9.1 and 10.2.3. + */ +var collatorInternalProperties = { + sortLocaleData: collatorSortLocaleData, + searchLocaleData: collatorSearchLocaleData, + relevantExtensionKeys: ["co", "kf", "kn"], +}; + +/** + * Returns the actual locale used when a collator for |locale| is constructed. + */ +function collatorActualLocale(locale) { + assert(typeof locale === "string", "locale should be string"); + + // If |locale| is the default locale (e.g. da-DK), but only supported + // through a fallback (da), we need to get the actual locale before we + // can call intl_isUpperCaseFirst. Also see intl_BestAvailableLocale. + return BestAvailableLocaleIgnoringDefault("Collator", locale); +} + +/** + * Returns the default caseFirst values for the given locale. The first + * element in the returned array denotes the default value per ES2017 Intl, + * 9.1 Internal slots of Service Constructors. + */ +function collatorSortCaseFirst(locale) { + var actualLocale = collatorActualLocale(locale); + if (intl_isUpperCaseFirst(actualLocale)) { + return ["upper", "false", "lower"]; + } + + // Default caseFirst values for all other languages. + return ["false", "lower", "upper"]; +} + +/** + * Returns the default caseFirst value for the given locale. + */ +function collatorSortCaseFirstDefault(locale) { + var actualLocale = collatorActualLocale(locale); + if (intl_isUpperCaseFirst(actualLocale)) { + return "upper"; + } + + // Default caseFirst value for all other languages. + return "false"; +} + +function collatorSortLocaleData() { + /* eslint-disable object-shorthand */ + return { + co: intl_availableCollations, + kn: function() { + return ["false", "true"]; + }, + kf: collatorSortCaseFirst, + default: { + co: function() { + // The first element of the collations array must be |null| + // per ES2017 Intl, 10.2.3 Internal Slots. + return null; + }, + kn: function() { + return "false"; + }, + kf: collatorSortCaseFirstDefault, + }, + }; + /* eslint-enable object-shorthand */ +} + +function collatorSearchLocaleData() { + /* eslint-disable object-shorthand */ + return { + co: function() { + return [null]; + }, + kn: function() { + return ["false", "true"]; + }, + kf: function() { + return ["false", "lower", "upper"]; + }, + default: { + co: function() { + return null; + }, + kn: function() { + return "false"; + }, + kf: function() { + return "false"; + }, + }, + }; + /* eslint-enable object-shorthand */ +} + +/** + * Create function to be cached and returned by Intl.Collator.prototype.compare. + * + * Spec: ECMAScript Internationalization API Specification, 10.3.3.1. + */ +function createCollatorCompare(collator) { + // This function is not inlined in $Intl_Collator_compare_get to avoid + // creating a call-object on each call to $Intl_Collator_compare_get. + return function(x, y) { + // Step 1 (implicit). + + // Step 2. + assert(IsObject(collator), "collatorCompareToBind called with non-object"); + assert( + intl_GuardToCollator(collator) !== null, + "collatorCompareToBind called with non-Collator" + ); + + // Steps 3-6 + var X = ToString(x); + var Y = ToString(y); + + // Step 7. + return intl_CompareStrings(collator, X, Y); + }; +} + +/** + * Returns a function bound to this Collator that compares x (converted to a + * String value) and y (converted to a String value), + * and returns a number less than 0 if x < y, 0 if x = y, or a number greater + * than 0 if x > y according to the sort order for the locale and collation + * options of this Collator object. + * + * Spec: ECMAScript Internationalization API Specification, 10.3.3. + */ +// Uncloned functions with `$` prefix are allocated as extended function +// to store the original name in `SetCanonicalName`. +function $Intl_Collator_compare_get() { + // Step 1. + var collator = this; + + // Steps 2-3. + if ( + !IsObject(collator) || + (collator = intl_GuardToCollator(collator)) === null + ) { + return callFunction( + intl_CallCollatorMethodIfWrapped, + this, + "$Intl_Collator_compare_get" + ); + } + + var internals = getCollatorInternals(collator); + + // Step 4. + if (internals.boundCompare === undefined) { + // Steps 4.a-c. + internals.boundCompare = createCollatorCompare(collator); + } + + // Step 5. + return internals.boundCompare; +} +SetCanonicalName($Intl_Collator_compare_get, "get compare"); + +/** + * Returns the resolved options for a Collator object. + * + * Spec: ECMAScript Internationalization API Specification, 10.3.4. + */ +function Intl_Collator_resolvedOptions() { + // Step 1. + var collator = this; + + // Steps 2-3. + if ( + !IsObject(collator) || + (collator = intl_GuardToCollator(collator)) === null + ) { + return callFunction( + intl_CallCollatorMethodIfWrapped, + this, + "Intl_Collator_resolvedOptions" + ); + } + + var internals = getCollatorInternals(collator); + + // Steps 4-5. + var result = { + locale: internals.locale, + usage: internals.usage, + sensitivity: internals.sensitivity, + ignorePunctuation: internals.ignorePunctuation, + collation: internals.collation, + numeric: internals.numeric, + caseFirst: internals.caseFirst, + }; + + // Step 6. + return result; +} diff --git a/js/src/builtin/intl/CommonFunctions.cpp b/js/src/builtin/intl/CommonFunctions.cpp new file mode 100644 index 0000000000..86f555baa5 --- /dev/null +++ b/js/src/builtin/intl/CommonFunctions.cpp @@ -0,0 +1,149 @@ +/* -*- 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/. */ + +/* Operations used to implement multiple Intl.* classes. */ + +#include "builtin/intl/CommonFunctions.h" + +#include "mozilla/Assertions.h" +#include "mozilla/intl/ICUError.h" +#include "mozilla/TextUtils.h" + +#include <algorithm> + +#include "gc/GCEnum.h" +#include "gc/ZoneAllocator.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_INTERNAL_INTL_ERROR +#include "js/Value.h" +#include "vm/JSAtomState.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/SelfHosting.h" +#include "vm/Stack.h" +#include "vm/StringType.h" + +#include "gc/GCContext-inl.h" + +bool js::intl::InitializeObject(JSContext* cx, JS::Handle<JSObject*> obj, + JS::Handle<PropertyName*> initializer, + JS::Handle<JS::Value> locales, + JS::Handle<JS::Value> options) { + FixedInvokeArgs<3> args(cx); + + args[0].setObject(*obj); + args[1].set(locales); + args[2].set(options); + + RootedValue ignored(cx); + if (!CallSelfHostedFunction(cx, initializer, JS::NullHandleValue, args, + &ignored)) { + return false; + } + + MOZ_ASSERT(ignored.isUndefined(), + "Unexpected return value from non-legacy Intl object initializer"); + return true; +} + +bool js::intl::LegacyInitializeObject(JSContext* cx, JS::Handle<JSObject*> obj, + JS::Handle<PropertyName*> initializer, + JS::Handle<JS::Value> thisValue, + JS::Handle<JS::Value> locales, + JS::Handle<JS::Value> options, + DateTimeFormatOptions dtfOptions, + JS::MutableHandle<JS::Value> result) { + FixedInvokeArgs<5> args(cx); + + args[0].setObject(*obj); + args[1].set(thisValue); + args[2].set(locales); + args[3].set(options); + args[4].setBoolean(dtfOptions == DateTimeFormatOptions::EnableMozExtensions); + + if (!CallSelfHostedFunction(cx, initializer, NullHandleValue, args, result)) { + return false; + } + + MOZ_ASSERT(result.isObject(), + "Legacy Intl object initializer must return an object"); + return true; +} + +JSObject* js::intl::GetInternalsObject(JSContext* cx, + JS::Handle<JSObject*> obj) { + FixedInvokeArgs<1> args(cx); + + args[0].setObject(*obj); + + RootedValue v(cx); + if (!js::CallSelfHostedFunction(cx, cx->names().getInternals, NullHandleValue, + args, &v)) { + return nullptr; + } + + return &v.toObject(); +} + +void js::intl::ReportInternalError(JSContext* cx) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INTERNAL_INTL_ERROR); +} + +void js::intl::ReportInternalError(JSContext* cx, + mozilla::intl::ICUError error) { + switch (error) { + case mozilla::intl::ICUError::OutOfMemory: + ReportOutOfMemory(cx); + return; + case mozilla::intl::ICUError::InternalError: + ReportInternalError(cx); + return; + case mozilla::intl::ICUError::OverflowError: + ReportAllocationOverflow(cx); + return; + } + MOZ_CRASH("Unexpected ICU error"); +} + +const js::intl::OldStyleLanguageTagMapping + js::intl::oldStyleLanguageTagMappings[] = { + {"pa-PK", "pa-Arab-PK"}, {"zh-CN", "zh-Hans-CN"}, + {"zh-HK", "zh-Hant-HK"}, {"zh-SG", "zh-Hans-SG"}, + {"zh-TW", "zh-Hant-TW"}, +}; + +js::UniqueChars js::intl::EncodeLocale(JSContext* cx, JSString* locale) { + MOZ_ASSERT(locale->length() > 0); + + js::UniqueChars chars = EncodeAscii(cx, locale); + +#ifdef DEBUG + // Ensure the returned value contains only valid BCP 47 characters. + // (Lambdas can't be placed inside MOZ_ASSERT, so move the checks in an + // #ifdef block.) + if (chars) { + auto alnumOrDash = [](char c) { + return mozilla::IsAsciiAlphanumeric(c) || c == '-'; + }; + MOZ_ASSERT(mozilla::IsAsciiAlpha(chars[0])); + MOZ_ASSERT( + std::all_of(chars.get(), chars.get() + locale->length(), alnumOrDash)); + } +#endif + + return chars; +} + +void js::intl::AddICUCellMemory(JSObject* obj, size_t nbytes) { + // Account the (estimated) number of bytes allocated by an ICU object against + // the JSObject's zone. + AddCellMemory(obj, nbytes, MemoryUse::ICUObject); +} + +void js::intl::RemoveICUCellMemory(JS::GCContext* gcx, JSObject* obj, + size_t nbytes) { + gcx->removeCellMemory(obj, nbytes, MemoryUse::ICUObject); +} diff --git a/js/src/builtin/intl/CommonFunctions.h b/js/src/builtin/intl/CommonFunctions.h new file mode 100644 index 0000000000..3f9681c2b7 --- /dev/null +++ b/js/src/builtin/intl/CommonFunctions.h @@ -0,0 +1,103 @@ +/* -*- 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 builtin_intl_CommonFunctions_h +#define builtin_intl_CommonFunctions_h + +#include <stddef.h> +#include <stdint.h> + +#include "js/RootingAPI.h" +#include "js/Utility.h" + +namespace mozilla::intl { +enum class ICUError : uint8_t; +} + +namespace js { + +class PropertyName; + +namespace intl { + +/** + * Initialize a new Intl.* object using the named self-hosted function. + */ +extern bool InitializeObject(JSContext* cx, JS::Handle<JSObject*> obj, + JS::Handle<PropertyName*> initializer, + JS::Handle<JS::Value> locales, + JS::Handle<JS::Value> options); + +enum class DateTimeFormatOptions { + Standard, + EnableMozExtensions, +}; + +/** + * Initialize an existing object as an Intl.* object using the named + * self-hosted function. This is only for a few old Intl.* constructors, for + * legacy reasons -- new ones should use the function above instead. + */ +extern bool LegacyInitializeObject(JSContext* cx, JS::Handle<JSObject*> obj, + JS::Handle<PropertyName*> initializer, + JS::Handle<JS::Value> thisValue, + JS::Handle<JS::Value> locales, + JS::Handle<JS::Value> options, + DateTimeFormatOptions dtfOptions, + JS::MutableHandle<JS::Value> result); + +/** + * Returns the object holding the internal properties for obj. + */ +extern JSObject* GetInternalsObject(JSContext* cx, JS::Handle<JSObject*> obj); + +/** Report an Intl internal error not directly tied to a spec step. */ +extern void ReportInternalError(JSContext* cx); + +/** Report an Intl internal error not directly tied to a spec step. */ +extern void ReportInternalError(JSContext* cx, mozilla::intl::ICUError error); + +/** + * The last-ditch locale is used if none of the available locales satisfies a + * request. "en-GB" is used based on the assumptions that English is the most + * common second language, that both en-GB and en-US are normally available in + * an implementation, and that en-GB is more representative of the English used + * in other locales. + */ +static inline const char* LastDitchLocale() { return "en-GB"; } + +/** + * Certain old, commonly-used language tags that lack a script, are expected to + * nonetheless imply one. This object maps these old-style tags to modern + * equivalents. + */ +struct OldStyleLanguageTagMapping { + const char* const oldStyle; + const char* const modernStyle; + + // Provide a constructor to catch missing initializers in the mappings array. + constexpr OldStyleLanguageTagMapping(const char* oldStyle, + const char* modernStyle) + : oldStyle(oldStyle), modernStyle(modernStyle) {} +}; + +extern const OldStyleLanguageTagMapping oldStyleLanguageTagMappings[5]; + +extern JS::UniqueChars EncodeLocale(JSContext* cx, JSString* locale); + +// The inline capacity we use for a Vector<char16_t>. Use this to ensure that +// our uses of ICU string functions, below and elsewhere, will try to fill the +// buffer's entire inline capacity before growing it and heap-allocating. +constexpr size_t INITIAL_CHAR_BUFFER_SIZE = 32; + +void AddICUCellMemory(JSObject* obj, size_t nbytes); + +void RemoveICUCellMemory(JS::GCContext* gcx, JSObject* obj, size_t nbytes); +} // namespace intl + +} // namespace js + +#endif /* builtin_intl_CommonFunctions_h */ diff --git a/js/src/builtin/intl/CommonFunctions.js b/js/src/builtin/intl/CommonFunctions.js new file mode 100644 index 0000000000..c369c49afa --- /dev/null +++ b/js/src/builtin/intl/CommonFunctions.js @@ -0,0 +1,992 @@ +/* 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/. */ + +/* Portions Copyright Norbert Lindenberg 2011-2012. */ + +#ifdef DEBUG +#define assertIsValidAndCanonicalLanguageTag(locale, desc) \ + do { \ + let canonical = intl_TryValidateAndCanonicalizeLanguageTag(locale); \ + assert(canonical !== null, \ + `${desc} is a structurally valid language tag`); \ + assert(canonical === locale, \ + `${desc} is a canonicalized language tag`); \ + } while (false) +#else +#define assertIsValidAndCanonicalLanguageTag(locale, desc) ; // Elided assertion. +#endif + +/** + * Returns the start index of a "Unicode locale extension sequence", which the + * specification defines as: "any substring of a language tag that starts with + * a separator '-' and the singleton 'u' and includes the maximum sequence of + * following non-singleton subtags and their preceding '-' separators." + * + * Alternatively, this may be defined as: the components of a language tag that + * match the `unicode_locale_extensions` production in UTS 35. + * + * Spec: ECMAScript Internationalization API Specification, 6.2.1. + */ +function startOfUnicodeExtensions(locale) { + assert(typeof locale === "string", "locale is a string"); + + // Search for "-u-" marking the start of a Unicode extension sequence. + var start = callFunction(std_String_indexOf, locale, "-u-"); + if (start < 0) { + return -1; + } + + // And search for "-x-" marking the start of any privateuse component to + // handle the case when "-u-" was only found within a privateuse subtag. + var privateExt = callFunction(std_String_indexOf, locale, "-x-"); + if (privateExt >= 0 && privateExt < start) { + return -1; + } + + return start; +} + +/** + * Returns the end index of a Unicode locale extension sequence. + */ +function endOfUnicodeExtensions(locale, start) { + assert(typeof locale === "string", "locale is a string"); + assert(0 <= start && start < locale.length, "start is an index into locale"); + assert( + Substring(locale, start, 3) === "-u-", + "start points to Unicode extension sequence" + ); + + // Search for the start of the next singleton or privateuse subtag. + // + // Begin searching after the smallest possible Unicode locale extension + // sequence, namely |"-u-" 2alphanum|. End searching once the remaining + // characters can't fit the smallest possible singleton or privateuse + // subtag, namely |"-x-" alphanum|. Note the reduced end-limit means + // indexing inside the loop is always in-range. + for (var i = start + 5, end = locale.length - 4; i <= end; i++) { + if (locale[i] !== "-") { + continue; + } + if (locale[i + 2] === "-") { + return i; + } + + // Skip over (i + 1) and (i + 2) because we've just verified they + // aren't "-", so the next possible delimiter can only be at (i + 3). + i += 2; + } + + // If no singleton or privateuse subtag was found, the Unicode extension + // sequence extends until the end of the string. + return locale.length; +} + +/** + * Removes Unicode locale extension sequences from the given language tag. + */ +function removeUnicodeExtensions(locale) { + assertIsValidAndCanonicalLanguageTag( + locale, + "locale with possible Unicode extension" + ); + + var start = startOfUnicodeExtensions(locale); + if (start < 0) { + return locale; + } + + var end = endOfUnicodeExtensions(locale, start); + + var left = Substring(locale, 0, start); + var right = Substring(locale, end, locale.length - end); + var combined = left + right; + + assertIsValidAndCanonicalLanguageTag(combined, "the recombined locale"); + assert( + startOfUnicodeExtensions(combined) < 0, + "recombination failed to remove all Unicode locale extension sequences" + ); + + return combined; +} + +/** + * Returns Unicode locale extension sequences from the given language tag. + */ +function getUnicodeExtensions(locale) { + assertIsValidAndCanonicalLanguageTag(locale, "locale with Unicode extension"); + + var start = startOfUnicodeExtensions(locale); + assert(start >= 0, "start of Unicode extension sequence not found"); + var end = endOfUnicodeExtensions(locale, start); + + return Substring(locale, start, end - start); +} + +/** + * Returns true if the input contains only ASCII alphabetical characters. + */ +function IsASCIIAlphaString(s) { + assert(typeof s === "string", "IsASCIIAlphaString"); + + for (var i = 0; i < s.length; i++) { + var c = callFunction(std_String_charCodeAt, s, i); + if (!((0x41 <= c && c <= 0x5a) || (0x61 <= c && c <= 0x7a))) { + return false; + } + } + return true; +} + +var localeCache = { + runtimeDefaultLocale: undefined, + defaultLocale: undefined, +}; + +/** + * Returns the BCP 47 language tag for the host environment's current locale. + * + * Spec: ECMAScript Internationalization API Specification, 6.2.4. + */ +function DefaultLocale() { + if (intl_IsRuntimeDefaultLocale(localeCache.runtimeDefaultLocale)) { + return localeCache.defaultLocale; + } + + // If we didn't have a cache hit, compute the candidate default locale. + var runtimeDefaultLocale = intl_RuntimeDefaultLocale(); + var locale = intl_supportedLocaleOrFallback(runtimeDefaultLocale); + + assertIsValidAndCanonicalLanguageTag(locale, "the computed default locale"); + assert( + startOfUnicodeExtensions(locale) < 0, + "the computed default locale must not contain a Unicode extension sequence" + ); + + // Cache the computed locale until the runtime default locale changes. + localeCache.defaultLocale = locale; + localeCache.runtimeDefaultLocale = runtimeDefaultLocale; + + return locale; +} + +/** + * Canonicalizes a locale list. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.1. + */ +function CanonicalizeLocaleList(locales) { + // Step 1. + if (locales === undefined) { + return []; + } + + // Step 3 (and the remaining steps). + var tag = intl_ValidateAndCanonicalizeLanguageTag(locales, false); + if (tag !== null) { + assert( + typeof tag === "string", + "intl_ValidateAndCanonicalizeLanguageTag returns a string value" + ); + return [tag]; + } + + // Step 2. + var seen = []; + + // Step 4. + var O = ToObject(locales); + + // Step 5. + var len = ToLength(O.length); + + // Step 6. + var k = 0; + + // Step 7. + while (k < len) { + // Steps 7.a-c. + if (k in O) { + // Step 7.c.i. + var kValue = O[k]; + + // Step 7.c.ii. + if (!(typeof kValue === "string" || IsObject(kValue))) { + ThrowTypeError(JSMSG_INVALID_LOCALES_ELEMENT); + } + + // Steps 7.c.iii-iv. + var tag = intl_ValidateAndCanonicalizeLanguageTag(kValue, true); + assert( + typeof tag === "string", + "ValidateAndCanonicalizeLanguageTag returns a string value" + ); + + // Step 7.c.v. + if (callFunction(std_Array_indexOf, seen, tag) === -1) { + DefineDataProperty(seen, seen.length, tag); + } + } + + // Step 7.d. + k++; + } + + // Step 8. + return seen; +} + +/** + * Compares a BCP 47 language tag against the locales in availableLocales + * and returns the best available match. Uses the fallback + * mechanism of RFC 4647, section 3.4. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.2. + * Spec: RFC 4647, section 3.4. + */ +function BestAvailableLocale(availableLocales, locale) { + return intl_BestAvailableLocale(availableLocales, locale, DefaultLocale()); +} + +/** + * Identical to BestAvailableLocale, but does not consider the default locale + * during computation. + */ +function BestAvailableLocaleIgnoringDefault(availableLocales, locale) { + return intl_BestAvailableLocale(availableLocales, locale, null); +} + +/** + * Compares a BCP 47 language priority list against the set of locales in + * availableLocales and determines the best available language to meet the + * request. Options specified through Unicode extension subsequences are + * ignored in the lookup, but information about such subsequences is returned + * separately. + * + * This variant is based on the Lookup algorithm of RFC 4647 section 3.4. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.3. + * Spec: RFC 4647, section 3.4. + */ +function LookupMatcher(availableLocales, requestedLocales) { + // Step 1. + var result = new_Record(); + + // Step 2. + for (var i = 0; i < requestedLocales.length; i++) { + var locale = requestedLocales[i]; + + // Step 2.a. + var noExtensionsLocale = removeUnicodeExtensions(locale); + + // Step 2.b. + var availableLocale = BestAvailableLocale( + availableLocales, + noExtensionsLocale + ); + + // Step 2.c. + if (availableLocale !== undefined) { + // Step 2.c.i. + result.locale = availableLocale; + + // Step 2.c.ii. + if (locale !== noExtensionsLocale) { + result.extension = getUnicodeExtensions(locale); + } + + // Step 2.c.iii. + return result; + } + } + + // Steps 3-4. + result.locale = DefaultLocale(); + + // Step 5. + return result; +} + +/** + * Compares a BCP 47 language priority list against the set of locales in + * availableLocales and determines the best available language to meet the + * request. Options specified through Unicode extension subsequences are + * ignored in the lookup, but information about such subsequences is returned + * separately. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.4. + */ +function BestFitMatcher(availableLocales, requestedLocales) { + // this implementation doesn't have anything better + return LookupMatcher(availableLocales, requestedLocales); +} + +/** + * Returns the Unicode extension value subtags for the requested key subtag. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.5. + */ +function UnicodeExtensionValue(extension, key) { + assert(typeof extension === "string", "extension is a string value"); + assert( + callFunction(std_String_startsWith, extension, "-u-") && + getUnicodeExtensions("und" + extension) === extension, + "extension is a Unicode extension subtag" + ); + assert(typeof key === "string", "key is a string value"); + + // Step 1. + assert(key.length === 2, "key is a Unicode extension key subtag"); + + // Step 2. + var size = extension.length; + + // Step 3. + var searchValue = "-" + key + "-"; + + // Step 4. + var pos = callFunction(std_String_indexOf, extension, searchValue); + + // Step 5. + if (pos !== -1) { + // Step 5.a. + var start = pos + 4; + + // Step 5.b. + var end = start; + + // Step 5.c. + var k = start; + + // Steps 5.d-e. + while (true) { + // Step 5.e.i. + var e = callFunction(std_String_indexOf, extension, "-", k); + + // Step 5.e.ii. + var len = e === -1 ? size - k : e - k; + + // Step 5.e.iii. + if (len === 2) { + break; + } + + // Step 5.e.iv. + if (e === -1) { + end = size; + break; + } + + // Step 5.e.v. + end = e; + k = e + 1; + } + + // Step 5.f. + return callFunction(String_substring, extension, start, end); + } + + // Step 6. + searchValue = "-" + key; + + // Steps 7-8. + if (callFunction(std_String_endsWith, extension, searchValue)) { + return ""; + } + + // Step 9 (implicit). +} + +/** + * Compares a BCP 47 language priority list against availableLocales and + * determines the best available language to meet the request. Options specified + * through Unicode extension subsequences are negotiated separately, taking the + * caller's relevant extensions and locale data as well as client-provided + * options into consideration. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.6. + */ +function ResolveLocale( + availableLocales, + requestedLocales, + options, + relevantExtensionKeys, + localeData +) { + // Steps 1-3. + var matcher = options.localeMatcher; + var r = + matcher === "lookup" + ? LookupMatcher(availableLocales, requestedLocales) + : BestFitMatcher(availableLocales, requestedLocales); + + // Step 4. + var foundLocale = r.locale; + var extension = r.extension; + + // Step 5. + var result = new_Record(); + + // Step 6. + result.dataLocale = foundLocale; + + // Step 7. + var supportedExtension = "-u"; + + // In this implementation, localeData is a function, not an object. + var localeDataProvider = localeData(); + + // Step 8. + for (var i = 0; i < relevantExtensionKeys.length; i++) { + var key = relevantExtensionKeys[i]; + + // Steps 8.a-h (The locale data is only computed when needed). + var keyLocaleData = undefined; + var value = undefined; + + // Locale tag may override. + + // Step 8.g. + var supportedExtensionAddition = ""; + + // Step 8.h. + if (extension !== undefined) { + // Step 8.h.i. + var requestedValue = UnicodeExtensionValue(extension, key); + + // Step 8.h.ii. + if (requestedValue !== undefined) { + // Steps 8.a-d. + keyLocaleData = callFunction( + localeDataProvider[key], + null, + foundLocale + ); + + // Step 8.h.ii.1. + if (requestedValue !== "") { + // Step 8.h.ii.1.a. + if ( + callFunction(std_Array_indexOf, keyLocaleData, requestedValue) !== + -1 + ) { + value = requestedValue; + supportedExtensionAddition = "-" + key + "-" + value; + } + } else { + // Step 8.h.ii.2. + + // According to the LDML spec, if there's no type value, + // and true is an allowed value, it's used. + if (callFunction(std_Array_indexOf, keyLocaleData, "true") !== -1) { + value = "true"; + supportedExtensionAddition = "-" + key; + } + } + } + } + + // Options override all. + + // Step 8.i.i. + var optionsValue = options[key]; + + // Step 8.i.ii. + assert( + typeof optionsValue === "string" || + optionsValue === undefined || + optionsValue === null, + "unexpected type for options value" + ); + + // Steps 8.i, 8.i.iii.1. + if (optionsValue !== undefined && optionsValue !== value) { + // Steps 8.a-d. + if (keyLocaleData === undefined) { + keyLocaleData = callFunction( + localeDataProvider[key], + null, + foundLocale + ); + } + + // Step 8.i.iii. + if (callFunction(std_Array_indexOf, keyLocaleData, optionsValue) !== -1) { + value = optionsValue; + supportedExtensionAddition = ""; + } + } + + // Locale data provides default value. + if (value === undefined) { + // Steps 8.a-f. + value = + keyLocaleData === undefined + ? callFunction(localeDataProvider.default[key], null, foundLocale) + : keyLocaleData[0]; + } + + // Step 8.j. + assert( + typeof value === "string" || value === null, + "unexpected locale data value" + ); + result[key] = value; + + // Step 8.k. + supportedExtension += supportedExtensionAddition; + } + + // Step 9. + if (supportedExtension.length > 2) { + foundLocale = addUnicodeExtension(foundLocale, supportedExtension); + } + + // Step 10. + result.locale = foundLocale; + + // Step 11. + return result; +} + +/** + * Adds a Unicode extension subtag to a locale. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.6. + */ +function addUnicodeExtension(locale, extension) { + assert(typeof locale === "string", "locale is a string value"); + assert( + !callFunction(std_String_startsWith, locale, "x-"), + "unexpected privateuse-only locale" + ); + assert( + startOfUnicodeExtensions(locale) < 0, + "Unicode extension subtag already present in locale" + ); + + assert(typeof extension === "string", "extension is a string value"); + assert( + callFunction(std_String_startsWith, extension, "-u-") && + getUnicodeExtensions("und" + extension) === extension, + "extension is a Unicode extension subtag" + ); + + // Step 9.a. + var privateIndex = callFunction(std_String_indexOf, locale, "-x-"); + + // Steps 9.b-c. + if (privateIndex === -1) { + locale += extension; + } else { + var preExtension = callFunction(String_substring, locale, 0, privateIndex); + var postExtension = callFunction(String_substring, locale, privateIndex); + locale = preExtension + extension + postExtension; + } + + // Steps 9.d-e (Step 9.e is not required in this implementation, because we don't canonicalize + // Unicode extension subtags). + assertIsValidAndCanonicalLanguageTag(locale, "locale after concatenation"); + + return locale; +} + +/** + * Returns the subset of requestedLocales for which availableLocales has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.7. + */ +function LookupSupportedLocales(availableLocales, requestedLocales) { + // Step 1. + var subset = []; + + // Step 2. + for (var i = 0; i < requestedLocales.length; i++) { + var locale = requestedLocales[i]; + + // Step 2.a. + var noExtensionsLocale = removeUnicodeExtensions(locale); + + // Step 2.b. + var availableLocale = BestAvailableLocale( + availableLocales, + noExtensionsLocale + ); + + // Step 2.c. + if (availableLocale !== undefined) { + DefineDataProperty(subset, subset.length, locale); + } + } + + // Step 3. + return subset; +} + +/** + * Returns the subset of requestedLocales for which availableLocales has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.8. + */ +function BestFitSupportedLocales(availableLocales, requestedLocales) { + // don't have anything better + return LookupSupportedLocales(availableLocales, requestedLocales); +} + +/** + * Returns the subset of requestedLocales for which availableLocales has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.9. + */ +function SupportedLocales(availableLocales, requestedLocales, options) { + // Step 1. + var matcher; + if (options !== undefined) { + // Step 1.a. + options = ToObject(options); + + // Step 1.b + matcher = options.localeMatcher; + if (matcher !== undefined) { + matcher = ToString(matcher); + if (matcher !== "lookup" && matcher !== "best fit") { + ThrowRangeError(JSMSG_INVALID_LOCALE_MATCHER, matcher); + } + } + } + + // Steps 2-5. + return matcher === undefined || matcher === "best fit" + ? BestFitSupportedLocales(availableLocales, requestedLocales) + : LookupSupportedLocales(availableLocales, requestedLocales); +} + +/** + * Extracts a property value from the provided options object, converts it to + * the required type, checks whether it is one of a list of allowed values, + * and fills in a fallback value if necessary. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.10. + */ +function GetOption(options, property, type, values, fallback) { + // Step 1. + var value = options[property]; + + // Step 2. + if (value !== undefined) { + // Steps 2.a-c. + if (type === "boolean") { + value = ToBoolean(value); + } else if (type === "string") { + value = ToString(value); + } else { + assert(false, "GetOption"); + } + + // Step 2.d. + if ( + values !== undefined && + callFunction(std_Array_indexOf, values, value) === -1 + ) { + ThrowRangeError(JSMSG_INVALID_OPTION_VALUE, property, `"${value}"`); + } + + // Step 2.e. + return value; + } + + // Step 3. + return fallback; +} + +/** + * Extracts a property value from the provided options object, converts it to + * a boolean or string, checks whether it is one of a list of allowed values, + * and fills in a fallback value if necessary. + */ +function GetStringOrBooleanOption( + options, + property, + values, + trueValue, + falsyValue, + fallback +) { + assert(IsObject(values), "GetStringOrBooleanOption"); + + // Step 1. + var value = options[property]; + + // Step 2. + if (value === undefined) { + return fallback; + } + + // Step 3. + if (value === true) { + return trueValue; + } + + // Steps 4-5. + if (!value) { + return falsyValue; + } + + // Step 6. + value = ToString(value); + + // Step 7. + if (callFunction(std_Array_indexOf, values, value) === -1) { + return fallback; + } + + // Step 8. + return value; +} + +/** + * The abstract operation DefaultNumberOption converts value to a Number value, + * checks whether it is in the allowed range, and fills in a fallback value if + * necessary. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.11. + */ +function DefaultNumberOption(value, minimum, maximum, fallback) { + assert( + typeof minimum === "number" && (minimum | 0) === minimum, + "DefaultNumberOption" + ); + assert( + typeof maximum === "number" && (maximum | 0) === maximum, + "DefaultNumberOption" + ); + assert( + fallback === undefined || + (typeof fallback === "number" && (fallback | 0) === fallback), + "DefaultNumberOption" + ); + assert( + fallback === undefined || (minimum <= fallback && fallback <= maximum), + "DefaultNumberOption" + ); + + // Step 1. + if (value === undefined) { + return fallback; + } + + // Step 2. + value = ToNumber(value); + + // Step 3. + if (Number_isNaN(value) || value < minimum || value > maximum) { + ThrowRangeError(JSMSG_INVALID_DIGITS_VALUE, value); + } + + // Step 4. + // Apply bitwise-or to convert -0 to +0 per ES2017, 5.2 and to ensure the + // result is an int32 value. + return std_Math_floor(value) | 0; +} + +/** + * Extracts a property value from the provided options object, converts it to a + * Number value, checks whether it is in the allowed range, and fills in a + * fallback value if necessary. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.12. + */ +function GetNumberOption(options, property, minimum, maximum, fallback) { + // Steps 1-2. + return DefaultNumberOption(options[property], minimum, maximum, fallback); +} + +// Symbols in the self-hosting compartment can't be cloned, use a separate +// object to hold the actual symbol value. +// TODO: Can we add support to clone symbols? +var intlFallbackSymbolHolder = { value: undefined }; + +/** + * The [[FallbackSymbol]] symbol of the %Intl% intrinsic object. + * + * This symbol is used to implement the legacy constructor semantics for + * Intl.DateTimeFormat and Intl.NumberFormat. + */ +function intlFallbackSymbol() { + var fallbackSymbol = intlFallbackSymbolHolder.value; + if (!fallbackSymbol) { + let Symbol = GetBuiltinConstructor("Symbol"); + fallbackSymbol = Symbol("IntlLegacyConstructedSymbol"); + intlFallbackSymbolHolder.value = fallbackSymbol; + } + return fallbackSymbol; +} + +/** + * Initializes the INTL_INTERNALS_OBJECT_SLOT of the given object. + */ +function initializeIntlObject(obj, type, lazyData) { + assert(IsObject(obj), "Non-object passed to initializeIntlObject"); + assert( + (type === "Collator" && intl_GuardToCollator(obj) !== null) || + (type === "DateTimeFormat" && intl_GuardToDateTimeFormat(obj) !== null) || + (type === "DisplayNames" && intl_GuardToDisplayNames(obj) !== null) || + (type === "ListFormat" && intl_GuardToListFormat(obj) !== null) || + (type === "NumberFormat" && intl_GuardToNumberFormat(obj) !== null) || + (type === "PluralRules" && intl_GuardToPluralRules(obj) !== null) || + (type === "RelativeTimeFormat" && + intl_GuardToRelativeTimeFormat(obj) !== null), + "type must match the object's class" + ); + assert(IsObject(lazyData), "non-object lazy data"); + + // The meaning of an internals object for an object |obj| is as follows. + // + // The .type property indicates the type of Intl object that |obj| is. It + // must be one of: + // - Collator + // - DateTimeFormat + // - DisplayNames + // - ListFormat + // - NumberFormat + // - PluralRules + // - RelativeTimeFormat + // + // The .lazyData property stores information needed to compute -- without + // observable side effects -- the actual internal Intl properties of + // |obj|. If it is non-null, then the actual internal properties haven't + // been computed, and .lazyData must be processed by + // |setInternalProperties| before internal Intl property values are + // available. If it is null, then the .internalProps property contains an + // object whose properties are the internal Intl properties of |obj|. + + var internals = std_Object_create(null); + internals.type = type; + internals.lazyData = lazyData; + internals.internalProps = null; + + assert( + UnsafeGetReservedSlot(obj, INTL_INTERNALS_OBJECT_SLOT) === undefined, + "Internal slot already initialized?" + ); + UnsafeSetReservedSlot(obj, INTL_INTERNALS_OBJECT_SLOT, internals); +} + +/** + * Set the internal properties object for an |internals| object previously + * associated with lazy data. + */ +function setInternalProperties(internals, internalProps) { + assert(IsObject(internals.lazyData), "lazy data must exist already"); + assert(IsObject(internalProps), "internalProps argument should be an object"); + + // Set in reverse order so that the .lazyData nulling is a barrier. + internals.internalProps = internalProps; + internals.lazyData = null; +} + +/** + * Get the existing internal properties out of a non-newborn |internals|, or + * null if none have been computed. + */ +function maybeInternalProperties(internals) { + assert(IsObject(internals), "non-object passed to maybeInternalProperties"); + var lazyData = internals.lazyData; + if (lazyData) { + return null; + } + assert( + IsObject(internals.internalProps), + "missing lazy data and computed internals" + ); + return internals.internalProps; +} + +/** + * Return |obj|'s internals object (*not* the object holding its internal + * properties!), with structure specified above. + * + * Spec: ECMAScript Internationalization API Specification, 10.3. + * Spec: ECMAScript Internationalization API Specification, 11.3. + * Spec: ECMAScript Internationalization API Specification, 12.3. + */ +function getIntlObjectInternals(obj) { + assert(IsObject(obj), "getIntlObjectInternals called with non-Object"); + assert( + intl_GuardToCollator(obj) !== null || + intl_GuardToDateTimeFormat(obj) !== null || + intl_GuardToDisplayNames(obj) !== null || + intl_GuardToListFormat(obj) !== null || + intl_GuardToNumberFormat(obj) !== null || + intl_GuardToPluralRules(obj) !== null || + intl_GuardToRelativeTimeFormat(obj) !== null, + "getIntlObjectInternals called with non-Intl object" + ); + + var internals = UnsafeGetReservedSlot(obj, INTL_INTERNALS_OBJECT_SLOT); + + assert(IsObject(internals), "internals not an object"); + assert(hasOwn("type", internals), "missing type"); + assert( + (internals.type === "Collator" && intl_GuardToCollator(obj) !== null) || + (internals.type === "DateTimeFormat" && + intl_GuardToDateTimeFormat(obj) !== null) || + (internals.type === "DisplayNames" && + intl_GuardToDisplayNames(obj) !== null) || + (internals.type === "ListFormat" && + intl_GuardToListFormat(obj) !== null) || + (internals.type === "NumberFormat" && + intl_GuardToNumberFormat(obj) !== null) || + (internals.type === "PluralRules" && + intl_GuardToPluralRules(obj) !== null) || + (internals.type === "RelativeTimeFormat" && + intl_GuardToRelativeTimeFormat(obj) !== null), + "type must match the object's class" + ); + assert(hasOwn("lazyData", internals), "missing lazyData"); + assert(hasOwn("internalProps", internals), "missing internalProps"); + + return internals; +} + +/** + * Get the internal properties of known-Intl object |obj|. For use only by + * C++ code that knows what it's doing! + */ +function getInternals(obj) { + var internals = getIntlObjectInternals(obj); + + // If internal properties have already been computed, use them. + var internalProps = maybeInternalProperties(internals); + if (internalProps) { + return internalProps; + } + + // Otherwise it's time to fully create them. + var type = internals.type; + if (type === "Collator") { + internalProps = resolveCollatorInternals(internals.lazyData); + } else if (type === "DateTimeFormat") { + internalProps = resolveDateTimeFormatInternals(internals.lazyData); + } else if (type === "DisplayNames") { + internalProps = resolveDisplayNamesInternals(internals.lazyData); + } else if (type === "ListFormat") { + internalProps = resolveListFormatInternals(internals.lazyData); + } else if (type === "NumberFormat") { + internalProps = resolveNumberFormatInternals(internals.lazyData); + } else if (type === "PluralRules") { + internalProps = resolvePluralRulesInternals(internals.lazyData); + } else { + internalProps = resolveRelativeTimeFormatInternals(internals.lazyData); + } + setInternalProperties(internals, internalProps); + return internalProps; +} diff --git a/js/src/builtin/intl/CurrencyDataGenerated.js b/js/src/builtin/intl/CurrencyDataGenerated.js new file mode 100644 index 0000000000..dcde004956 --- /dev/null +++ b/js/src/builtin/intl/CurrencyDataGenerated.js @@ -0,0 +1,78 @@ +// Generated by make_intl_data.py. DO NOT EDIT. +// Version: 2023-01-01 + +/** + * Mapping from currency codes to the number of decimal digits used for them. + * Default is 2 digits. + * + * Spec: ISO 4217 Currency and Funds Code List. + * http://www.currency-iso.org/en/home/tables/table-a1.html + */ +var currencyDigits = { + // Bahraini Dinar (BAHRAIN) + BHD: 3, + // Burundi Franc (BURUNDI) + BIF: 0, + // Unidad de Fomento (CHILE) + CLF: 4, + // Chilean Peso (CHILE) + CLP: 0, + // Djibouti Franc (DJIBOUTI) + DJF: 0, + // Guinean Franc (GUINEA) + GNF: 0, + // Iraqi Dinar (IRAQ) + IQD: 3, + // Iceland Krona (ICELAND) + ISK: 0, + // Jordanian Dinar (JORDAN) + JOD: 3, + // Yen (JAPAN) + JPY: 0, + // Comorian Franc (COMOROS (THE)) + KMF: 0, + // Won (KOREA (THE REPUBLIC OF)) + KRW: 0, + // Kuwaiti Dinar (KUWAIT) + KWD: 3, + // Libyan Dinar (LIBYA) + LYD: 3, + // Rial Omani (OMAN) + OMR: 3, + // Guarani (PARAGUAY) + PYG: 0, + // Rwanda Franc (RWANDA) + RWF: 0, + // Tunisian Dinar (TUNISIA) + TND: 3, + // Uganda Shilling (UGANDA) + UGX: 0, + // Uruguay Peso en Unidades Indexadas (UI) (URUGUAY) + UYI: 0, + // Unidad Previsional (URUGUAY) + UYW: 4, + // Dong (VIET NAM) + VND: 0, + // Vatu (VANUATU) + VUV: 0, + // CFA Franc BEAC (CAMEROON) + // CFA Franc BEAC (CENTRAL AFRICAN REPUBLIC (THE)) + // CFA Franc BEAC (CHAD) + // CFA Franc BEAC (CONGO (THE)) + // CFA Franc BEAC (EQUATORIAL GUINEA) + // CFA Franc BEAC (GABON) + XAF: 0, + // CFA Franc BCEAO (BENIN) + // CFA Franc BCEAO (BURKINA FASO) + // CFA Franc BCEAO (CÔTE D'IVOIRE) + // CFA Franc BCEAO (GUINEA-BISSAU) + // CFA Franc BCEAO (MALI) + // CFA Franc BCEAO (NIGER (THE)) + // CFA Franc BCEAO (SENEGAL) + // CFA Franc BCEAO (TOGO) + XOF: 0, + // CFP Franc (FRENCH POLYNESIA) + // CFP Franc (NEW CALEDONIA) + // CFP Franc (WALLIS AND FUTUNA) + XPF: 0, +}; diff --git a/js/src/builtin/intl/DateTimeFormat.cpp b/js/src/builtin/intl/DateTimeFormat.cpp new file mode 100644 index 0000000000..8327a47422 --- /dev/null +++ b/js/src/builtin/intl/DateTimeFormat.cpp @@ -0,0 +1,1567 @@ +/* -*- 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/. */ + +/* Intl.DateTimeFormat implementation. */ + +#include "builtin/intl/DateTimeFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/intl/Calendar.h" +#include "mozilla/intl/DateIntervalFormat.h" +#include "mozilla/intl/DateTimeFormat.h" +#include "mozilla/intl/DateTimePart.h" +#include "mozilla/intl/Locale.h" +#include "mozilla/intl/TimeZone.h" +#include "mozilla/Range.h" +#include "mozilla/Span.h" + +#include "builtin/Array.h" +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/FormatBuffer.h" +#include "builtin/intl/LanguageTag.h" +#include "builtin/intl/SharedIntlData.h" +#include "gc/GCContext.h" +#include "js/Date.h" +#include "js/experimental/Intl.h" // JS::AddMozDateTimeFormatConstructor +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/GCAPI.h" +#include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperties +#include "js/PropertySpec.h" +#include "js/StableStringChars.h" +#include "vm/DateTime.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/Runtime.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/GeckoProfiler-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +using JS::AutoStableStringChars; +using JS::ClippedTime; +using JS::TimeClip; + +using js::intl::DateTimeFormatOptions; +using js::intl::FormatBuffer; +using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::SharedIntlData; + +const JSClassOps DateTimeFormatObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + DateTimeFormatObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass DateTimeFormatObject::class_ = { + "Intl.DateTimeFormat", + JSCLASS_HAS_RESERVED_SLOTS(DateTimeFormatObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_DateTimeFormat) | + JSCLASS_FOREGROUND_FINALIZE, + &DateTimeFormatObject::classOps_, &DateTimeFormatObject::classSpec_}; + +const JSClass& DateTimeFormatObject::protoClass_ = PlainObject::class_; + +static bool dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().DateTimeFormat); + return true; +} + +static const JSFunctionSpec dateTimeFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", + "Intl_DateTimeFormat_supportedLocalesOf", 1, 0), + JS_FS_END}; + +static const JSFunctionSpec dateTimeFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DateTimeFormat_resolvedOptions", + 0, 0), + JS_SELF_HOSTED_FN("formatToParts", "Intl_DateTimeFormat_formatToParts", 1, + 0), + JS_SELF_HOSTED_FN("formatRange", "Intl_DateTimeFormat_formatRange", 2, 0), + JS_SELF_HOSTED_FN("formatRangeToParts", + "Intl_DateTimeFormat_formatRangeToParts", 2, 0), + JS_FN(js_toSource_str, dateTimeFormat_toSource, 0, 0), + JS_FS_END}; + +static const JSPropertySpec dateTimeFormat_properties[] = { + JS_SELF_HOSTED_GET("format", "$Intl_DateTimeFormat_format_get", 0), + JS_STRING_SYM_PS(toStringTag, "Intl.DateTimeFormat", JSPROP_READONLY), + JS_PS_END}; + +static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp); + +const ClassSpec DateTimeFormatObject::classSpec_ = { + GenericCreateConstructor<DateTimeFormat, 0, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<DateTimeFormatObject>, + dateTimeFormat_static_methods, + nullptr, + dateTimeFormat_methods, + dateTimeFormat_properties, + nullptr, + ClassSpec::DontDefineConstructor}; + +/** + * 12.2.1 Intl.DateTimeFormat([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct, + DateTimeFormatOptions dtfOptions) { + AutoJSConstructorProfilerEntry pseudoFrame(cx, "Intl.DateTimeFormat"); + + // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). + + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + JSProtoKey protoKey = dtfOptions == DateTimeFormatOptions::Standard + ? JSProto_DateTimeFormat + : JSProto_Null; + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, protoKey, &proto)) { + return false; + } + + Rooted<DateTimeFormatObject*> dateTimeFormat(cx); + dateTimeFormat = NewObjectWithClassProto<DateTimeFormatObject>(cx, proto); + if (!dateTimeFormat) { + return false; + } + + RootedValue thisValue( + cx, construct ? ObjectValue(*dateTimeFormat) : args.thisv()); + HandleValue locales = args.get(0); + HandleValue options = args.get(1); + + // Step 3. + return intl::LegacyInitializeObject( + cx, dateTimeFormat, cx->names().InitializeDateTimeFormat, thisValue, + locales, options, dtfOptions, args.rval()); +} + +static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return DateTimeFormat(cx, args, args.isConstructing(), + DateTimeFormatOptions::Standard); +} + +static bool MozDateTimeFormat(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Don't allow to call mozIntl.DateTimeFormat as a function. That way we + // don't need to worry how to handle the legacy initialization semantics + // when applied on mozIntl.DateTimeFormat. + if (!ThrowIfNotConstructing(cx, args, "mozIntl.DateTimeFormat")) { + return false; + } + + return DateTimeFormat(cx, args, true, + DateTimeFormatOptions::EnableMozExtensions); +} + +bool js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + // intl_DateTimeFormat is an intrinsic for self-hosted JavaScript, so it + // cannot be used with "new", but it still has to be treated as a + // constructor. + return DateTimeFormat(cx, args, true, DateTimeFormatOptions::Standard); +} + +void js::DateTimeFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + + auto* dateTimeFormat = &obj->as<DateTimeFormatObject>(); + mozilla::intl::DateTimeFormat* df = dateTimeFormat->getDateFormat(); + mozilla::intl::DateIntervalFormat* dif = + dateTimeFormat->getDateIntervalFormat(); + + if (df) { + intl::RemoveICUCellMemory( + gcx, obj, DateTimeFormatObject::UDateFormatEstimatedMemoryUse); + + delete df; + } + + if (dif) { + intl::RemoveICUCellMemory( + gcx, obj, DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse); + + delete dif; + } +} + +bool JS::AddMozDateTimeFormatConstructor(JSContext* cx, + JS::Handle<JSObject*> intl) { + RootedObject ctor( + cx, GlobalObject::createConstructor(cx, MozDateTimeFormat, + cx->names().DateTimeFormat, 0)); + if (!ctor) { + return false; + } + + RootedObject proto( + cx, GlobalObject::createBlankPrototype<PlainObject>(cx, cx->global())); + if (!proto) { + return false; + } + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) { + return false; + } + + // 12.3.2 + if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) { + return false; + } + + // 12.4.4 and 12.4.5 + if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) { + return false; + } + + // 12.4.2 and 12.4.3 + if (!JS_DefineProperties(cx, proto, dateTimeFormat_properties)) { + return false; + } + + RootedValue ctorValue(cx, ObjectValue(*ctor)); + return DefineDataProperty(cx, intl, cx->names().DateTimeFormat, ctorValue, 0); +} + +static bool DefaultCalendar(JSContext* cx, const UniqueChars& locale, + MutableHandleValue rval) { + auto calendar = mozilla::intl::Calendar::TryCreate(locale.get()); + if (calendar.isErr()) { + intl::ReportInternalError(cx, calendar.unwrapErr()); + return false; + } + + auto type = calendar.unwrap()->GetBcp47Type(); + if (type.isErr()) { + intl::ReportInternalError(cx, type.unwrapErr()); + return false; + } + + JSString* str = NewStringCopy<CanGC>(cx, type.unwrap()); + if (!str) { + return false; + } + + rval.setString(str); + return true; +} + +bool js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + UniqueChars locale = intl::EncodeLocale(cx, args[0].toString()); + if (!locale) { + return false; + } + + RootedObject calendars(cx, NewDenseEmptyArray(cx)); + if (!calendars) { + return false; + } + + // We need the default calendar for the locale as the first result. + RootedValue defaultCalendar(cx); + if (!DefaultCalendar(cx, locale, &defaultCalendar)) { + return false; + } + + if (!NewbornArrayPush(cx, calendars, defaultCalendar)) { + return false; + } + + // Now get the calendars that "would make a difference", i.e., not the + // default. + auto keywords = + mozilla::intl::Calendar::GetBcp47KeywordValuesForLocale(locale.get()); + if (keywords.isErr()) { + intl::ReportInternalError(cx, keywords.unwrapErr()); + return false; + } + + for (auto keyword : keywords.unwrap()) { + if (keyword.isErr()) { + intl::ReportInternalError(cx); + return false; + } + + JSString* jscalendar = NewStringCopy<CanGC>(cx, keyword.unwrap()); + if (!jscalendar) { + return false; + } + if (!NewbornArrayPush(cx, calendars, StringValue(jscalendar))) { + return false; + } + } + + args.rval().setObject(*calendars); + return true; +} + +bool js::intl_defaultCalendar(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + UniqueChars locale = intl::EncodeLocale(cx, args[0].toString()); + if (!locale) { + return false; + } + + return DefaultCalendar(cx, locale, args.rval()); +} + +bool js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + + RootedString timeZone(cx, args[0].toString()); + Rooted<JSAtom*> validatedTimeZone(cx); + if (!sharedIntlData.validateTimeZoneName(cx, timeZone, &validatedTimeZone)) { + return false; + } + + if (validatedTimeZone) { + cx->markAtom(validatedTimeZone); + args.rval().setString(validatedTimeZone); + } else { + args.rval().setNull(); + } + + return true; +} + +bool js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + + // Some time zone names are canonicalized differently by ICU -- handle + // those first: + RootedString timeZone(cx, args[0].toString()); + Rooted<JSAtom*> ianaTimeZone(cx); + if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA( + cx, timeZone, &ianaTimeZone)) { + return false; + } + + if (ianaTimeZone) { + cx->markAtom(ianaTimeZone); + args.rval().setString(ianaTimeZone); + return true; + } + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, timeZone)) { + return false; + } + + FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> canonicalTimeZone(cx); + auto result = mozilla::intl::TimeZone::GetCanonicalTimeZoneID( + stableChars.twoByteRange(), canonicalTimeZone); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + JSString* str = canonicalTimeZone.toString(cx); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +bool js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> timeZone(cx); + auto result = + DateTimeInfo::timeZoneId(DateTimeInfo::shouldRFP(cx->realm()), timeZone); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + JSString* str = timeZone.toString(cx); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +bool js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + auto offset = + DateTimeInfo::getRawOffsetMs(DateTimeInfo::shouldRFP(cx->realm())); + if (offset.isErr()) { + intl::ReportInternalError(cx, offset.unwrapErr()); + return false; + } + + args.rval().setInt32(offset.unwrap()); + return true; +} + +bool js::intl_isDefaultTimeZone(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString() || args[0].isUndefined()); + + // |undefined| is the default value when the Intl runtime caches haven't + // yet been initialized. Handle it the same way as a cache miss. + if (args[0].isUndefined()) { + args.rval().setBoolean(false); + return true; + } + + FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> chars(cx); + auto result = + DateTimeInfo::timeZoneId(DateTimeInfo::shouldRFP(cx->realm()), chars); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + JSLinearString* str = args[0].toString()->ensureLinear(cx); + if (!str) { + return false; + } + + bool equals; + if (str->length() == chars.length()) { + JS::AutoCheckCannotGC nogc; + equals = + str->hasLatin1Chars() + ? EqualChars(str->latin1Chars(nogc), chars.data(), str->length()) + : EqualChars(str->twoByteChars(nogc), chars.data(), str->length()); + } else { + equals = false; + } + + args.rval().setBoolean(equals); + return true; +} + +enum class HourCycle { + // 12 hour cycle, from 0 to 11. + H11, + + // 12 hour cycle, from 1 to 12. + H12, + + // 24 hour cycle, from 0 to 23. + H23, + + // 24 hour cycle, from 1 to 24. + H24 +}; + +static UniqueChars DateTimeFormatLocale( + JSContext* cx, HandleObject internals, + mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle> hourCycle = + mozilla::Nothing()) { + RootedValue value(cx); + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) { + return nullptr; + } + + // ICU expects calendar, numberingSystem, and hourCycle as Unicode locale + // extensions on locale. + + mozilla::intl::Locale tag; + { + Rooted<JSLinearString*> locale(cx, value.toString()->ensureLinear(cx)); + if (!locale) { + return nullptr; + } + + if (!intl::ParseLocale(cx, locale, tag)) { + return nullptr; + } + } + + JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx); + + if (!GetProperty(cx, internals, internals, cx->names().calendar, &value)) { + return nullptr; + } + + { + JSLinearString* calendar = value.toString()->ensureLinear(cx); + if (!calendar) { + return nullptr; + } + + if (!keywords.emplaceBack("ca", calendar)) { + return nullptr; + } + } + + if (!GetProperty(cx, internals, internals, cx->names().numberingSystem, + &value)) { + return nullptr; + } + + { + JSLinearString* numberingSystem = value.toString()->ensureLinear(cx); + if (!numberingSystem) { + return nullptr; + } + + if (!keywords.emplaceBack("nu", numberingSystem)) { + return nullptr; + } + } + + if (hourCycle) { + JSAtom* hourCycleStr; + switch (*hourCycle) { + case mozilla::intl::DateTimeFormat::HourCycle::H11: + hourCycleStr = cx->names().h11; + break; + case mozilla::intl::DateTimeFormat::HourCycle::H12: + hourCycleStr = cx->names().h12; + break; + case mozilla::intl::DateTimeFormat::HourCycle::H23: + hourCycleStr = cx->names().h23; + break; + case mozilla::intl::DateTimeFormat::HourCycle::H24: + hourCycleStr = cx->names().h24; + break; + } + + if (!keywords.emplaceBack("hc", hourCycleStr)) { + return nullptr; + } + } + + // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of + // the Unicode extension subtag. We're then relying on ICU to follow RFC + // 6067, which states that any trailing keywords using the same key + // should be ignored. + if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) { + return nullptr; + } + + FormatBuffer<char> buffer(cx); + if (auto result = tag.ToString(buffer); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + return buffer.extractStringZ(); +} + +static bool AssignTextComponent( + JSContext* cx, HandleObject internals, Handle<PropertyName*> property, + mozilla::Maybe<mozilla::intl::DateTimeFormat::Text>* text) { + RootedValue value(cx); + if (!GetProperty(cx, internals, internals, property, &value)) { + return false; + } + + if (value.isString()) { + JSLinearString* string = value.toString()->ensureLinear(cx); + if (!string) { + return false; + } + if (StringEqualsLiteral(string, "narrow")) { + *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Narrow); + } else if (StringEqualsLiteral(string, "short")) { + *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Short); + } else { + MOZ_ASSERT(StringEqualsLiteral(string, "long")); + *text = mozilla::Some(mozilla::intl::DateTimeFormat::Text::Long); + } + } else { + MOZ_ASSERT(value.isUndefined()); + } + + return true; +} + +static bool AssignNumericComponent( + JSContext* cx, HandleObject internals, Handle<PropertyName*> property, + mozilla::Maybe<mozilla::intl::DateTimeFormat::Numeric>* numeric) { + RootedValue value(cx); + if (!GetProperty(cx, internals, internals, property, &value)) { + return false; + } + + if (value.isString()) { + JSLinearString* string = value.toString()->ensureLinear(cx); + if (!string) { + return false; + } + if (StringEqualsLiteral(string, "numeric")) { + *numeric = mozilla::Some(mozilla::intl::DateTimeFormat::Numeric::Numeric); + } else { + MOZ_ASSERT(StringEqualsLiteral(string, "2-digit")); + *numeric = + mozilla::Some(mozilla::intl::DateTimeFormat::Numeric::TwoDigit); + } + } else { + MOZ_ASSERT(value.isUndefined()); + } + + return true; +} + +static bool AssignMonthComponent( + JSContext* cx, HandleObject internals, Handle<PropertyName*> property, + mozilla::Maybe<mozilla::intl::DateTimeFormat::Month>* month) { + RootedValue value(cx); + if (!GetProperty(cx, internals, internals, property, &value)) { + return false; + } + + if (value.isString()) { + JSLinearString* string = value.toString()->ensureLinear(cx); + if (!string) { + return false; + } + if (StringEqualsLiteral(string, "numeric")) { + *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Numeric); + } else if (StringEqualsLiteral(string, "2-digit")) { + *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::TwoDigit); + } else if (StringEqualsLiteral(string, "long")) { + *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Long); + } else if (StringEqualsLiteral(string, "short")) { + *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Short); + } else { + MOZ_ASSERT(StringEqualsLiteral(string, "narrow")); + *month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Narrow); + } + } else { + MOZ_ASSERT(value.isUndefined()); + } + + return true; +} + +static bool AssignTimeZoneNameComponent( + JSContext* cx, HandleObject internals, Handle<PropertyName*> property, + mozilla::Maybe<mozilla::intl::DateTimeFormat::TimeZoneName>* tzName) { + RootedValue value(cx); + if (!GetProperty(cx, internals, internals, property, &value)) { + return false; + } + + if (value.isString()) { + JSLinearString* string = value.toString()->ensureLinear(cx); + if (!string) { + return false; + } + if (StringEqualsLiteral(string, "long")) { + *tzName = + mozilla::Some(mozilla::intl::DateTimeFormat::TimeZoneName::Long); + } else if (StringEqualsLiteral(string, "short")) { + *tzName = + mozilla::Some(mozilla::intl::DateTimeFormat::TimeZoneName::Short); + } else if (StringEqualsLiteral(string, "shortOffset")) { + *tzName = mozilla::Some( + mozilla::intl::DateTimeFormat::TimeZoneName::ShortOffset); + } else if (StringEqualsLiteral(string, "longOffset")) { + *tzName = mozilla::Some( + mozilla::intl::DateTimeFormat::TimeZoneName::LongOffset); + } else if (StringEqualsLiteral(string, "shortGeneric")) { + *tzName = mozilla::Some( + mozilla::intl::DateTimeFormat::TimeZoneName::ShortGeneric); + } else { + MOZ_ASSERT(StringEqualsLiteral(string, "longGeneric")); + *tzName = mozilla::Some( + mozilla::intl::DateTimeFormat::TimeZoneName::LongGeneric); + } + } else { + MOZ_ASSERT(value.isUndefined()); + } + + return true; +} + +static bool AssignHourCycleComponent( + JSContext* cx, HandleObject internals, Handle<PropertyName*> property, + mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle>* hourCycle) { + RootedValue value(cx); + if (!GetProperty(cx, internals, internals, property, &value)) { + return false; + } + + if (value.isString()) { + JSLinearString* string = value.toString()->ensureLinear(cx); + if (!string) { + return false; + } + if (StringEqualsLiteral(string, "h11")) { + *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H11); + } else if (StringEqualsLiteral(string, "h12")) { + *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H12); + } else if (StringEqualsLiteral(string, "h23")) { + *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H23); + } else { + MOZ_ASSERT(StringEqualsLiteral(string, "h24")); + *hourCycle = mozilla::Some(mozilla::intl::DateTimeFormat::HourCycle::H24); + } + } else { + MOZ_ASSERT(value.isUndefined()); + } + + return true; +} + +static bool AssignHour12Component(JSContext* cx, HandleObject internals, + mozilla::Maybe<bool>* hour12) { + RootedValue value(cx); + if (!GetProperty(cx, internals, internals, cx->names().hour12, &value)) { + return false; + } + if (value.isBoolean()) { + *hour12 = mozilla::Some(value.toBoolean()); + } else { + MOZ_ASSERT(value.isUndefined()); + } + + return true; +} + +static bool AssignDateTimeLength( + JSContext* cx, HandleObject internals, Handle<PropertyName*> property, + mozilla::Maybe<mozilla::intl::DateTimeFormat::Style>* style) { + RootedValue value(cx); + if (!GetProperty(cx, internals, internals, property, &value)) { + return false; + } + + if (value.isString()) { + JSLinearString* string = value.toString()->ensureLinear(cx); + if (!string) { + return false; + } + if (StringEqualsLiteral(string, "full")) { + *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Full); + } else if (StringEqualsLiteral(string, "long")) { + *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long); + } else if (StringEqualsLiteral(string, "medium")) { + *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Medium); + } else { + MOZ_ASSERT(StringEqualsLiteral(string, "short")); + *style = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short); + } + } else { + MOZ_ASSERT(value.isUndefined()); + } + + return true; +} + +/** + * Returns a new mozilla::intl::DateTimeFormat with the locale and date-time + * formatting options of the given DateTimeFormat. + */ +static mozilla::intl::DateTimeFormat* NewDateTimeFormat( + JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat) { + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat)); + if (!internals) { + return nullptr; + } + + UniqueChars locale = DateTimeFormatLocale(cx, internals); + if (!locale) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) { + return nullptr; + } + + AutoStableStringChars timeZone(cx); + if (!timeZone.initTwoByte(cx, value.toString())) { + return nullptr; + } + + mozilla::Range<const char16_t> timeZoneChars = timeZone.twoByteRange(); + + if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) { + return nullptr; + } + bool hasPattern = value.isString(); + + if (!GetProperty(cx, internals, internals, cx->names().timeStyle, &value)) { + return nullptr; + } + bool hasStyle = value.isString(); + if (!hasStyle) { + if (!GetProperty(cx, internals, internals, cx->names().dateStyle, &value)) { + return nullptr; + } + hasStyle = value.isString(); + } + + mozilla::UniquePtr<mozilla::intl::DateTimeFormat> df = nullptr; + if (hasPattern) { + // This is a DateTimeFormat defined by a pattern option. This is internal + // to Mozilla, and not part of the ECMA-402 API. + if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) { + return nullptr; + } + + AutoStableStringChars pattern(cx); + if (!pattern.initTwoByte(cx, value.toString())) { + return nullptr; + } + + auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromPattern( + mozilla::MakeStringSpan(locale.get()), pattern.twoByteRange(), + mozilla::Some(timeZoneChars)); + if (dfResult.isErr()) { + intl::ReportInternalError(cx, dfResult.unwrapErr()); + return nullptr; + } + + df = dfResult.unwrap(); + } else if (hasStyle) { + // This is a DateTimeFormat defined by a time style or date style. + mozilla::intl::DateTimeFormat::StyleBag style; + if (!AssignDateTimeLength(cx, internals, cx->names().timeStyle, + &style.time)) { + return nullptr; + } + if (!AssignDateTimeLength(cx, internals, cx->names().dateStyle, + &style.date)) { + return nullptr; + } + if (!AssignHourCycleComponent(cx, internals, cx->names().hourCycle, + &style.hourCycle)) { + return nullptr; + } + + if (!AssignHour12Component(cx, internals, &style.hour12)) { + return nullptr; + } + + SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + + mozilla::intl::DateTimePatternGenerator* gen = + sharedIntlData.getDateTimePatternGenerator(cx, locale.get()); + if (!gen) { + return nullptr; + } + auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromStyle( + mozilla::MakeStringSpan(locale.get()), style, gen, + mozilla::Some(timeZoneChars)); + if (dfResult.isErr()) { + intl::ReportInternalError(cx, dfResult.unwrapErr()); + return nullptr; + } + df = dfResult.unwrap(); + } else { + // This is a DateTimeFormat defined by a components bag. + mozilla::intl::DateTimeFormat::ComponentsBag bag; + + if (!AssignTextComponent(cx, internals, cx->names().era, &bag.era)) { + return nullptr; + } + if (!AssignNumericComponent(cx, internals, cx->names().year, &bag.year)) { + return nullptr; + } + if (!AssignMonthComponent(cx, internals, cx->names().month, &bag.month)) { + return nullptr; + } + if (!AssignNumericComponent(cx, internals, cx->names().day, &bag.day)) { + return nullptr; + } + if (!AssignTextComponent(cx, internals, cx->names().weekday, + &bag.weekday)) { + return nullptr; + } + if (!AssignNumericComponent(cx, internals, cx->names().hour, &bag.hour)) { + return nullptr; + } + if (!AssignNumericComponent(cx, internals, cx->names().minute, + &bag.minute)) { + return nullptr; + } + if (!AssignNumericComponent(cx, internals, cx->names().second, + &bag.second)) { + return nullptr; + } + if (!AssignTimeZoneNameComponent(cx, internals, cx->names().timeZoneName, + &bag.timeZoneName)) { + return nullptr; + } + if (!AssignHourCycleComponent(cx, internals, cx->names().hourCycle, + &bag.hourCycle)) { + return nullptr; + } + if (!AssignTextComponent(cx, internals, cx->names().dayPeriod, + &bag.dayPeriod)) { + return nullptr; + } + if (!AssignHour12Component(cx, internals, &bag.hour12)) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, + cx->names().fractionalSecondDigits, &value)) { + return nullptr; + } + if (value.isInt32()) { + bag.fractionalSecondDigits = mozilla::Some(value.toInt32()); + } else { + MOZ_ASSERT(value.isUndefined()); + } + + SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + auto* dtpg = sharedIntlData.getDateTimePatternGenerator(cx, locale.get()); + if (!dtpg) { + return nullptr; + } + + auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromComponents( + mozilla::MakeStringSpan(locale.get()), bag, dtpg, + mozilla::Some(timeZoneChars)); + if (dfResult.isErr()) { + intl::ReportInternalError(cx, dfResult.unwrapErr()); + return nullptr; + } + df = dfResult.unwrap(); + } + + // ECMAScript requires the Gregorian calendar to be used from the beginning + // of ECMAScript time. + df->SetStartTimeIfGregorian(StartOfTime); + + return df.release(); +} + +static mozilla::intl::DateTimeFormat* GetOrCreateDateTimeFormat( + JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat) { + // Obtain a cached mozilla::intl::DateTimeFormat object. + mozilla::intl::DateTimeFormat* df = dateTimeFormat->getDateFormat(); + if (df) { + return df; + } + + df = NewDateTimeFormat(cx, dateTimeFormat); + if (!df) { + return nullptr; + } + dateTimeFormat->setDateFormat(df); + + intl::AddICUCellMemory(dateTimeFormat, + DateTimeFormatObject::UDateFormatEstimatedMemoryUse); + return df; +} + +template <typename T> +static bool SetResolvedProperty(JSContext* cx, HandleObject resolved, + Handle<PropertyName*> name, + mozilla::Maybe<T> intlProp) { + if (!intlProp) { + return true; + } + JSString* str = NewStringCopyZ<CanGC>( + cx, mozilla::intl::DateTimeFormat::ToString(*intlProp)); + if (!str) { + return false; + } + RootedValue value(cx, StringValue(str)); + return DefineDataProperty(cx, resolved, name, value); +} + +bool js::intl_resolveDateTimeFormatComponents(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(args[1].isObject()); + MOZ_ASSERT(args[2].isBoolean()); + + Rooted<DateTimeFormatObject*> dateTimeFormat(cx); + dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>(); + + RootedObject resolved(cx, &args[1].toObject()); + + bool includeDateTimeFields = args[2].toBoolean(); + + mozilla::intl::DateTimeFormat* df = + GetOrCreateDateTimeFormat(cx, dateTimeFormat); + if (!df) { + return false; + } + + auto result = df->ResolveComponents(); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + mozilla::intl::DateTimeFormat::ComponentsBag components = result.unwrap(); + + // Map the resolved mozilla::intl::DateTimeFormat::ComponentsBag to the + // options object as returned by DateTimeFormat.prototype.resolvedOptions. + // + // Resolved options must match the ordering as defined in: + // https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.resolvedoptions + + if (!SetResolvedProperty(cx, resolved, cx->names().hourCycle, + components.hourCycle)) { + return false; + } + + if (components.hour12) { + RootedValue value(cx, BooleanValue(*components.hour12)); + if (!DefineDataProperty(cx, resolved, cx->names().hour12, value)) { + return false; + } + } + + if (!includeDateTimeFields) { + args.rval().setUndefined(); + // Do not include date time fields. + return true; + } + + if (!SetResolvedProperty(cx, resolved, cx->names().weekday, + components.weekday)) { + return false; + } + if (!SetResolvedProperty(cx, resolved, cx->names().era, components.era)) { + return false; + } + if (!SetResolvedProperty(cx, resolved, cx->names().year, components.year)) { + return false; + } + if (!SetResolvedProperty(cx, resolved, cx->names().month, components.month)) { + return false; + } + if (!SetResolvedProperty(cx, resolved, cx->names().day, components.day)) { + return false; + } + if (!SetResolvedProperty(cx, resolved, cx->names().dayPeriod, + components.dayPeriod)) { + return false; + } + if (!SetResolvedProperty(cx, resolved, cx->names().hour, components.hour)) { + return false; + } + if (!SetResolvedProperty(cx, resolved, cx->names().minute, + components.minute)) { + return false; + } + if (!SetResolvedProperty(cx, resolved, cx->names().second, + components.second)) { + return false; + } + if (!SetResolvedProperty(cx, resolved, cx->names().timeZoneName, + components.timeZoneName)) { + return false; + } + + if (components.fractionalSecondDigits) { + RootedValue value(cx, Int32Value(*components.fractionalSecondDigits)); + if (!DefineDataProperty(cx, resolved, cx->names().fractionalSecondDigits, + value)) { + return false; + } + } + + args.rval().setUndefined(); + return true; +} + +static bool intl_FormatDateTime(JSContext* cx, + const mozilla::intl::DateTimeFormat* df, + ClippedTime x, MutableHandleValue result) { + MOZ_ASSERT(x.isValid()); + + FormatBuffer<char16_t, INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + auto dfResult = df->TryFormat(x.toDouble(), buffer); + if (dfResult.isErr()) { + intl::ReportInternalError(cx, dfResult.unwrapErr()); + return false; + } + + JSString* str = buffer.toString(cx); + if (!str) { + return false; + } + + result.setString(str); + return true; +} + +using FieldType = js::ImmutableTenuredPtr<PropertyName*> JSAtomState::*; + +static FieldType GetFieldTypeForPartType(mozilla::intl::DateTimePartType type) { + switch (type) { + case mozilla::intl::DateTimePartType::Literal: + return &JSAtomState::literal; + case mozilla::intl::DateTimePartType::Era: + return &JSAtomState::era; + case mozilla::intl::DateTimePartType::Year: + return &JSAtomState::year; + case mozilla::intl::DateTimePartType::YearName: + return &JSAtomState::yearName; + case mozilla::intl::DateTimePartType::RelatedYear: + return &JSAtomState::relatedYear; + case mozilla::intl::DateTimePartType::Month: + return &JSAtomState::month; + case mozilla::intl::DateTimePartType::Day: + return &JSAtomState::day; + case mozilla::intl::DateTimePartType::Hour: + return &JSAtomState::hour; + case mozilla::intl::DateTimePartType::Minute: + return &JSAtomState::minute; + case mozilla::intl::DateTimePartType::Second: + return &JSAtomState::second; + case mozilla::intl::DateTimePartType::Weekday: + return &JSAtomState::weekday; + case mozilla::intl::DateTimePartType::DayPeriod: + return &JSAtomState::dayPeriod; + case mozilla::intl::DateTimePartType::TimeZoneName: + return &JSAtomState::timeZoneName; + case mozilla::intl::DateTimePartType::FractionalSecondDigits: + return &JSAtomState::fractionalSecond; + case mozilla::intl::DateTimePartType::Unknown: + return &JSAtomState::unknown; + } + + MOZ_CRASH( + "unenumerated, undocumented format field returned " + "by iterator"); +} + +static FieldType GetFieldTypeForPartSource( + mozilla::intl::DateTimePartSource source) { + switch (source) { + case mozilla::intl::DateTimePartSource::Shared: + return &JSAtomState::shared; + case mozilla::intl::DateTimePartSource::StartRange: + return &JSAtomState::startRange; + case mozilla::intl::DateTimePartSource::EndRange: + return &JSAtomState::endRange; + } + + MOZ_CRASH( + "unenumerated, undocumented format field returned " + "by iterator"); +} + +// A helper function to create an ArrayObject from DateTimePart objects. +// When hasNoSource is true, we don't need to create the ||Source|| property for +// the DateTimePart object. +static bool CreateDateTimePartArray( + JSContext* cx, mozilla::Span<const char16_t> formattedSpan, + bool hasNoSource, const mozilla::intl::DateTimePartVector& parts, + MutableHandleValue result) { + RootedString overallResult(cx, NewStringCopy<CanGC>(cx, formattedSpan)); + if (!overallResult) { + return false; + } + + Rooted<ArrayObject*> partsArray( + cx, NewDenseFullyAllocatedArray(cx, parts.length())); + if (!partsArray) { + return false; + } + partsArray->ensureDenseInitializedLength(0, parts.length()); + + if (overallResult->length() == 0) { + // An empty string contains no parts, so avoid extra work below. + result.setObject(*partsArray); + return true; + } + + RootedObject singlePart(cx); + RootedValue val(cx); + + size_t index = 0; + size_t beginIndex = 0; + for (const mozilla::intl::DateTimePart& part : parts) { + singlePart = NewPlainObject(cx); + if (!singlePart) { + return false; + } + + FieldType type = GetFieldTypeForPartType(part.mType); + val = StringValue(cx->names().*type); + if (!DefineDataProperty(cx, singlePart, cx->names().type, val)) { + return false; + } + + MOZ_ASSERT(part.mEndIndex > beginIndex); + JSLinearString* partStr = NewDependentString(cx, overallResult, beginIndex, + part.mEndIndex - beginIndex); + if (!partStr) { + return false; + } + val = StringValue(partStr); + if (!DefineDataProperty(cx, singlePart, cx->names().value, val)) { + return false; + } + + if (!hasNoSource) { + FieldType source = GetFieldTypeForPartSource(part.mSource); + val = StringValue(cx->names().*source); + if (!DefineDataProperty(cx, singlePart, cx->names().source, val)) { + return false; + } + } + + beginIndex = part.mEndIndex; + partsArray->initDenseElement(index++, ObjectValue(*singlePart)); + } + + MOZ_ASSERT(index == parts.length()); + MOZ_ASSERT(beginIndex == formattedSpan.size()); + result.setObject(*partsArray); + return true; +} + +static bool intl_FormatToPartsDateTime(JSContext* cx, + const mozilla::intl::DateTimeFormat* df, + ClippedTime x, bool hasNoSource, + MutableHandleValue result) { + MOZ_ASSERT(x.isValid()); + + FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + mozilla::intl::DateTimePartVector parts; + auto r = df->TryFormatToParts(x.toDouble(), buffer, parts); + if (r.isErr()) { + intl::ReportInternalError(cx, r.unwrapErr()); + return false; + } + + return CreateDateTimePartArray(cx, buffer, hasNoSource, parts, result); +} + +bool js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(args[1].isNumber()); + MOZ_ASSERT(args[2].isBoolean()); + + Rooted<DateTimeFormatObject*> dateTimeFormat(cx); + dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>(); + + bool formatToParts = args[2].toBoolean(); + + ClippedTime x = TimeClip(args[1].toNumber()); + if (!x.isValid()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DATE_NOT_FINITE, "DateTimeFormat", + formatToParts ? "formatToParts" : "format"); + return false; + } + + mozilla::intl::DateTimeFormat* df = + GetOrCreateDateTimeFormat(cx, dateTimeFormat); + if (!df) { + return false; + } + + // Use the DateTimeFormat to actually format the time stamp. + return formatToParts ? intl_FormatToPartsDateTime( + cx, df, x, /* hasNoSource */ true, args.rval()) + : intl_FormatDateTime(cx, df, x, args.rval()); +} + +/** + * Returns a new DateIntervalFormat with the locale and date-time formatting + * options of the given DateTimeFormat. + */ +static mozilla::intl::DateIntervalFormat* NewDateIntervalFormat( + JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat, + mozilla::intl::DateTimeFormat& mozDtf) { + RootedValue value(cx); + RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat)); + if (!internals) { + return nullptr; + } + + FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> pattern(cx); + auto result = mozDtf.GetPattern(pattern); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + + // Determine the hour cycle used in the resolved pattern. + mozilla::Maybe<mozilla::intl::DateTimeFormat::HourCycle> hcPattern = + mozilla::intl::DateTimeFormat::HourCycleFromPattern(pattern); + + UniqueChars locale = DateTimeFormatLocale(cx, internals, hcPattern); + if (!locale) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) { + return nullptr; + } + + AutoStableStringChars timeZone(cx); + if (!timeZone.initTwoByte(cx, value.toString())) { + return nullptr; + } + mozilla::Span<const char16_t> timeZoneChars = timeZone.twoByteRange(); + + FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> skeleton(cx); + auto skelResult = mozDtf.GetOriginalSkeleton(skeleton); + if (skelResult.isErr()) { + intl::ReportInternalError(cx, skelResult.unwrapErr()); + return nullptr; + } + + auto dif = mozilla::intl::DateIntervalFormat::TryCreate( + mozilla::MakeStringSpan(locale.get()), skeleton, timeZoneChars); + + if (dif.isErr()) { + js::intl::ReportInternalError(cx, dif.unwrapErr()); + return nullptr; + } + + return dif.unwrap().release(); +} + +static mozilla::intl::DateIntervalFormat* GetOrCreateDateIntervalFormat( + JSContext* cx, Handle<DateTimeFormatObject*> dateTimeFormat, + mozilla::intl::DateTimeFormat& mozDtf) { + // Obtain a cached DateIntervalFormat object. + mozilla::intl::DateIntervalFormat* dif = + dateTimeFormat->getDateIntervalFormat(); + if (dif) { + return dif; + } + + dif = NewDateIntervalFormat(cx, dateTimeFormat, mozDtf); + if (!dif) { + return nullptr; + } + dateTimeFormat->setDateIntervalFormat(dif); + + intl::AddICUCellMemory( + dateTimeFormat, + DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse); + return dif; +} + +/** + * PartitionDateTimeRangePattern ( dateTimeFormat, x, y ) + */ +static bool PartitionDateTimeRangePattern( + JSContext* cx, const mozilla::intl::DateTimeFormat* df, + const mozilla::intl::DateIntervalFormat* dif, + mozilla::intl::AutoFormattedDateInterval& formatted, ClippedTime x, + ClippedTime y, bool* equal) { + MOZ_ASSERT(x.isValid()); + MOZ_ASSERT(y.isValid()); + + // We can't access the calendar used by UDateIntervalFormat to change it to a + // proleptic Gregorian calendar. Instead we need to call a different formatter + // function which accepts UCalendar instead of UDate. + // But creating new UCalendar objects for each call is slow, so when we can + // ensure that the input dates are later than the Gregorian change date, + // directly call the formatter functions taking UDate. + + // The Gregorian change date "1582-10-15T00:00:00.000Z". + constexpr double GregorianChangeDate = -12219292800000.0; + + // Add a full day to account for time zone offsets. + constexpr double GregorianChangeDatePlusOneDay = + GregorianChangeDate + msPerDay; + + mozilla::intl::ICUResult result = Ok(); + if (x.toDouble() < GregorianChangeDatePlusOneDay || + y.toDouble() < GregorianChangeDatePlusOneDay) { + // Create calendar objects for the start and end date by cloning the date + // formatter calendar. The date formatter calendar already has the correct + // time zone set and was changed to use a proleptic Gregorian calendar. + auto startCal = df->CloneCalendar(x.toDouble()); + if (startCal.isErr()) { + intl::ReportInternalError(cx, startCal.unwrapErr()); + return false; + } + + auto endCal = df->CloneCalendar(y.toDouble()); + if (endCal.isErr()) { + intl::ReportInternalError(cx, endCal.unwrapErr()); + return false; + } + + result = dif->TryFormatCalendar(*startCal.unwrap(), *endCal.unwrap(), + formatted, equal); + } else { + // The common fast path which doesn't require creating calendar objects. + result = + dif->TryFormatDateTime(x.toDouble(), y.toDouble(), formatted, equal); + } + + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + return true; +} + +/** + * FormatDateTimeRange( dateTimeFormat, x, y ) + */ +static bool FormatDateTimeRange(JSContext* cx, + const mozilla::intl::DateTimeFormat* df, + const mozilla::intl::DateIntervalFormat* dif, + ClippedTime x, ClippedTime y, + MutableHandleValue result) { + mozilla::intl::AutoFormattedDateInterval formatted; + if (!formatted.IsValid()) { + intl::ReportInternalError(cx, formatted.GetError()); + return false; + } + + bool equal; + if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, x, y, &equal)) { + return false; + } + + // PartitionDateTimeRangePattern, step 12. + if (equal) { + return intl_FormatDateTime(cx, df, x, result); + } + + auto spanResult = formatted.ToSpan(); + if (spanResult.isErr()) { + intl::ReportInternalError(cx, spanResult.unwrapErr()); + return false; + } + JSString* resultStr = NewStringCopy<CanGC>(cx, spanResult.unwrap()); + if (!resultStr) { + return false; + } + + result.setString(resultStr); + return true; +} + +/** + * FormatDateTimeRangeToParts ( dateTimeFormat, x, y ) + */ +static bool FormatDateTimeRangeToParts( + JSContext* cx, const mozilla::intl::DateTimeFormat* df, + const mozilla::intl::DateIntervalFormat* dif, ClippedTime x, ClippedTime y, + MutableHandleValue result) { + mozilla::intl::AutoFormattedDateInterval formatted; + if (!formatted.IsValid()) { + intl::ReportInternalError(cx, formatted.GetError()); + return false; + } + + bool equal; + if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, x, y, &equal)) { + return false; + } + + // PartitionDateTimeRangePattern, step 12. + if (equal) { + return intl_FormatToPartsDateTime(cx, df, x, /* hasNoSource */ false, + result); + } + + mozilla::intl::DateTimePartVector parts; + auto r = dif->TryFormattedToParts(formatted, parts); + if (r.isErr()) { + intl::ReportInternalError(cx, r.unwrapErr()); + return false; + } + + auto spanResult = formatted.ToSpan(); + if (spanResult.isErr()) { + intl::ReportInternalError(cx, spanResult.unwrapErr()); + return false; + } + return CreateDateTimePartArray(cx, spanResult.unwrap(), + /* hasNoSource */ false, parts, result); +} + +bool js::intl_FormatDateTimeRange(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 4); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(args[1].isNumber()); + MOZ_ASSERT(args[2].isNumber()); + MOZ_ASSERT(args[3].isBoolean()); + + Rooted<DateTimeFormatObject*> dateTimeFormat(cx); + dateTimeFormat = &args[0].toObject().as<DateTimeFormatObject>(); + + bool formatToParts = args[3].toBoolean(); + + // PartitionDateTimeRangePattern, steps 1-2. + ClippedTime x = TimeClip(args[1].toNumber()); + if (!x.isValid()) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, "DateTimeFormat", + formatToParts ? "formatRangeToParts" : "formatRange"); + return false; + } + + // PartitionDateTimeRangePattern, steps 3-4. + ClippedTime y = TimeClip(args[2].toNumber()); + if (!y.isValid()) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, "DateTimeFormat", + formatToParts ? "formatRangeToParts" : "formatRange"); + return false; + } + + mozilla::intl::DateTimeFormat* df = + GetOrCreateDateTimeFormat(cx, dateTimeFormat); + if (!df) { + return false; + } + + mozilla::intl::DateIntervalFormat* dif = + GetOrCreateDateIntervalFormat(cx, dateTimeFormat, *df); + if (!dif) { + return false; + } + + // Use the DateIntervalFormat to actually format the time range. + return formatToParts + ? FormatDateTimeRangeToParts(cx, df, dif, x, y, args.rval()) + : FormatDateTimeRange(cx, df, dif, x, y, args.rval()); +} diff --git a/js/src/builtin/intl/DateTimeFormat.h b/js/src/builtin/intl/DateTimeFormat.h new file mode 100644 index 0000000000..f269f14282 --- /dev/null +++ b/js/src/builtin/intl/DateTimeFormat.h @@ -0,0 +1,188 @@ +/* -*- 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 builtin_intl_DateTimeFormat_h +#define builtin_intl_DateTimeFormat_h + +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "vm/NativeObject.h" + +namespace mozilla::intl { +class DateTimeFormat; +class DateIntervalFormat; +} // namespace mozilla::intl + +namespace js { + +class DateTimeFormatObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t DATE_FORMAT_SLOT = 1; + static constexpr uint32_t DATE_INTERVAL_FORMAT_SLOT = 2; + static constexpr uint32_t SLOT_COUNT = 3; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals " + "object slot"); + + // Estimated memory use for UDateFormat (see IcuMemoryUsage). + static constexpr size_t UDateFormatEstimatedMemoryUse = 72440; + + // Estimated memory use for UDateIntervalFormat (see IcuMemoryUsage). + static constexpr size_t UDateIntervalFormatEstimatedMemoryUse = 175646; + + mozilla::intl::DateTimeFormat* getDateFormat() const { + const auto& slot = getFixedSlot(DATE_FORMAT_SLOT); + if (slot.isUndefined()) { + return nullptr; + } + return static_cast<mozilla::intl::DateTimeFormat*>(slot.toPrivate()); + } + + void setDateFormat(mozilla::intl::DateTimeFormat* dateFormat) { + setFixedSlot(DATE_FORMAT_SLOT, PrivateValue(dateFormat)); + } + + mozilla::intl::DateIntervalFormat* getDateIntervalFormat() const { + const auto& slot = getFixedSlot(DATE_INTERVAL_FORMAT_SLOT); + if (slot.isUndefined()) { + return nullptr; + } + return static_cast<mozilla::intl::DateIntervalFormat*>(slot.toPrivate()); + } + + void setDateIntervalFormat( + mozilla::intl::DateIntervalFormat* dateIntervalFormat) { + setFixedSlot(DATE_INTERVAL_FORMAT_SLOT, PrivateValue(dateIntervalFormat)); + } + + private: + static const JSClassOps classOps_; + static const ClassSpec classSpec_; + + static void finalize(JS::GCContext* gcx, JSObject* obj); +}; + +/** + * Returns a new instance of the standard built-in DateTimeFormat constructor. + * Self-hosted code cannot cache this constructor (as it does for others in + * Utilities.js) because it is initialized after self-hosted code is compiled. + * + * Usage: dateTimeFormat = intl_DateTimeFormat(locales, options) + */ +[[nodiscard]] extern bool intl_DateTimeFormat(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * Returns an array with the calendar type identifiers per Unicode + * Technical Standard 35, Unicode Locale Data Markup Language, for the + * supported calendars for the given locale. The default calendar is + * element 0. + * + * Usage: calendars = intl_availableCalendars(locale) + */ +[[nodiscard]] extern bool intl_availableCalendars(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * Returns the calendar type identifier per Unicode Technical Standard 35, + * Unicode Locale Data Markup Language, for the default calendar for the given + * locale. + * + * Usage: calendar = intl_defaultCalendar(locale) + */ +[[nodiscard]] extern bool intl_defaultCalendar(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * 6.4.1 IsValidTimeZoneName ( timeZone ) + * + * Verifies that the given string is a valid time zone name. If it is a valid + * time zone name, its IANA time zone name is returned. Otherwise returns null. + * + * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 + * + * Usage: ianaTimeZone = intl_IsValidTimeZoneName(timeZone) + */ +[[nodiscard]] extern bool intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * Return the canonicalized time zone name. Canonicalization resolves link + * names to their target time zones. + * + * Usage: ianaTimeZone = intl_canonicalizeTimeZone(timeZone) + */ +[[nodiscard]] extern bool intl_canonicalizeTimeZone(JSContext* cx, + unsigned argc, + JS::Value* vp); + +/** + * Return the default time zone name. The time zone name is not canonicalized. + * + * Usage: icuDefaultTimeZone = intl_defaultTimeZone() + */ +[[nodiscard]] extern bool intl_defaultTimeZone(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * Return the raw offset from GMT in milliseconds for the default time zone. + * + * Usage: defaultTimeZoneOffset = intl_defaultTimeZoneOffset() + */ +[[nodiscard]] extern bool intl_defaultTimeZoneOffset(JSContext* cx, + unsigned argc, + JS::Value* vp); + +/** + * Return true if the given string is the default time zone as returned by + * intl_defaultTimeZone(). Otherwise return false. + * + * Usage: isIcuDefaultTimeZone = intl_isDefaultTimeZone(icuDefaultTimeZone) + */ +[[nodiscard]] extern bool intl_isDefaultTimeZone(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * Returns a String value representing x (which must be a Number value) + * according to the effective locale and the formatting options of the + * given DateTimeFormat. + * + * Spec: ECMAScript Internationalization API Specification, 12.3.2. + * + * Usage: formatted = intl_FormatDateTime(dateTimeFormat, x, formatToParts) + */ +[[nodiscard]] extern bool intl_FormatDateTime(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * Returns a String value representing the range between x and y (which both + * must be Number values) according to the effective locale and the formatting + * options of the given DateTimeFormat. + * + * Spec: Intl.DateTimeFormat.prototype.formatRange proposal + * + * Usage: formatted = intl_FormatDateTimeRange(dateTimeFmt, x, y, formatToParts) + */ +[[nodiscard]] extern bool intl_FormatDateTimeRange(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * Extracts the resolved components from a DateTimeFormat and applies them to + * the object for resolved components. + * + * Usage: intl_resolveDateTimeFormatComponents(dateTimeFormat, resolved) + */ +[[nodiscard]] extern bool intl_resolveDateTimeFormatComponents(JSContext* cx, + unsigned argc, + JS::Value* vp); +} // namespace js + +#endif /* builtin_intl_DateTimeFormat_h */ diff --git a/js/src/builtin/intl/DateTimeFormat.js b/js/src/builtin/intl/DateTimeFormat.js new file mode 100644 index 0000000000..c3c14ebfc2 --- /dev/null +++ b/js/src/builtin/intl/DateTimeFormat.js @@ -0,0 +1,1008 @@ +/* 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/. */ + +/* Portions Copyright Norbert Lindenberg 2011-2012. */ + +/** + * Compute an internal properties object from |lazyDateTimeFormatData|. + */ +function resolveDateTimeFormatInternals(lazyDateTimeFormatData) { + assert(IsObject(lazyDateTimeFormatData), "lazy data not an object?"); + + // Lazy DateTimeFormat data has the following structure: + // + // { + // requestedLocales: List of locales, + // + // localeOpt: // *first* opt computed in InitializeDateTimeFormat + // { + // localeMatcher: "lookup" / "best fit", + // + // ca: string matching a Unicode extension type, // optional + // + // nu: string matching a Unicode extension type, // optional + // + // hc: "h11" / "h12" / "h23" / "h24", // optional + // } + // + // timeZone: IANA time zone name, + // + // formatOpt: // *second* opt computed in InitializeDateTimeFormat + // { + // // all the properties/values listed in Table 3 + // // (weekday, era, year, month, day, &c.) + // + // hour12: true / false, // optional + // } + // + // formatMatcher: "basic" / "best fit", + // + // dateStyle: "full" / "long" / "medium" / "short" / undefined, + // + // timeStyle: "full" / "long" / "medium" / "short" / undefined, + // + // patternOption: + // String representing LDML Date Format pattern or undefined + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every DateTimeFormat lazy data object has *all* these properties, + // never a subset of them. + + var internalProps = std_Object_create(null); + + var DateTimeFormat = dateTimeFormatInternalProperties; + + // Compute effective locale. + + // Step 10. + var localeData = DateTimeFormat.localeData; + + // Step 11. + var r = ResolveLocale( + "DateTimeFormat", + lazyDateTimeFormatData.requestedLocales, + lazyDateTimeFormatData.localeOpt, + DateTimeFormat.relevantExtensionKeys, + localeData + ); + + // Steps 12-13, 15. + internalProps.locale = r.locale; + internalProps.calendar = r.ca; + internalProps.numberingSystem = r.nu; + + // Step 20. + internalProps.timeZone = lazyDateTimeFormatData.timeZone; + + // Step 21. + var formatOpt = lazyDateTimeFormatData.formatOpt; + + // Step 14. + // Copy the hourCycle setting, if present, to the format options. But + // only do this if no hour12 option is present, because the latter takes + // precedence over hourCycle. + if (r.hc !== null && formatOpt.hour12 === undefined) { + formatOpt.hourCycle = r.hc; + } + + // Steps 26-31, more or less - see comment after this function. + if (lazyDateTimeFormatData.patternOption !== undefined) { + internalProps.pattern = lazyDateTimeFormatData.patternOption; + } else if ( + lazyDateTimeFormatData.dateStyle !== undefined || + lazyDateTimeFormatData.timeStyle !== undefined + ) { + internalProps.hourCycle = formatOpt.hourCycle; + internalProps.hour12 = formatOpt.hour12; + internalProps.dateStyle = lazyDateTimeFormatData.dateStyle; + internalProps.timeStyle = lazyDateTimeFormatData.timeStyle; + } else { + internalProps.hourCycle = formatOpt.hourCycle; + internalProps.hour12 = formatOpt.hour12; + internalProps.weekday = formatOpt.weekday; + internalProps.era = formatOpt.era; + internalProps.year = formatOpt.year; + internalProps.month = formatOpt.month; + internalProps.day = formatOpt.day; + internalProps.dayPeriod = formatOpt.dayPeriod; + internalProps.hour = formatOpt.hour; + internalProps.minute = formatOpt.minute; + internalProps.second = formatOpt.second; + internalProps.fractionalSecondDigits = formatOpt.fractionalSecondDigits; + internalProps.timeZoneName = formatOpt.timeZoneName; + } + + // The caller is responsible for associating |internalProps| with the right + // object using |setInternalProperties|. + return internalProps; +} + +/** + * Returns an object containing the DateTimeFormat internal properties of |obj|. + */ +function getDateTimeFormatInternals(obj) { + assert(IsObject(obj), "getDateTimeFormatInternals called with non-object"); + assert( + intl_GuardToDateTimeFormat(obj) !== null, + "getDateTimeFormatInternals called with non-DateTimeFormat" + ); + + var internals = getIntlObjectInternals(obj); + assert( + internals.type === "DateTimeFormat", + "bad type escaped getIntlObjectInternals" + ); + + // If internal properties have already been computed, use them. + var internalProps = maybeInternalProperties(internals); + if (internalProps) { + return internalProps; + } + + // Otherwise it's time to fully create them. + internalProps = resolveDateTimeFormatInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * 12.1.10 UnwrapDateTimeFormat( dtf ) + */ +function UnwrapDateTimeFormat(dtf) { + // Steps 2 and 4 (error handling moved to caller). + if ( + IsObject(dtf) && + intl_GuardToDateTimeFormat(dtf) === null && + !intl_IsWrappedDateTimeFormat(dtf) && + callFunction( + std_Object_isPrototypeOf, + GetBuiltinPrototype("DateTimeFormat"), + dtf + ) + ) { + dtf = dtf[intlFallbackSymbol()]; + } + return dtf; +} + +/** + * 6.4.2 CanonicalizeTimeZoneName ( timeZone ) + * + * Canonicalizes the given IANA time zone name. + * + * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 + */ +function CanonicalizeTimeZoneName(timeZone) { + assert(typeof timeZone === "string", "CanonicalizeTimeZoneName"); + + // Step 1. (Not applicable, the input is already a valid IANA time zone.) + assert(timeZone !== "Etc/Unknown", "Invalid time zone"); + assert( + timeZone === intl_IsValidTimeZoneName(timeZone), + "Time zone name not normalized" + ); + + // Step 2. + var ianaTimeZone = intl_canonicalizeTimeZone(timeZone); + assert(ianaTimeZone !== "Etc/Unknown", "Invalid canonical time zone"); + assert( + ianaTimeZone === intl_IsValidTimeZoneName(ianaTimeZone), + "Unsupported canonical time zone" + ); + + // Step 3. + if (ianaTimeZone === "Etc/UTC" || ianaTimeZone === "Etc/GMT") { + ianaTimeZone = "UTC"; + } + + // Step 4. + return ianaTimeZone; +} + +var timeZoneCache = { + icuDefaultTimeZone: undefined, + defaultTimeZone: undefined, +}; + +/** + * 6.4.3 DefaultTimeZone () + * + * Returns the IANA time zone name for the host environment's current time zone. + * + * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 + */ +function DefaultTimeZone() { + if (intl_isDefaultTimeZone(timeZoneCache.icuDefaultTimeZone)) { + return timeZoneCache.defaultTimeZone; + } + + // Verify that the current ICU time zone is a valid ECMA-402 time zone. + var icuDefaultTimeZone = intl_defaultTimeZone(); + var timeZone = intl_IsValidTimeZoneName(icuDefaultTimeZone); + if (timeZone === null) { + // Before defaulting to "UTC", try to represent the default time zone + // using the Etc/GMT + offset format. This format only accepts full + // hour offsets. + const msPerHour = 60 * 60 * 1000; + var offset = intl_defaultTimeZoneOffset(); + assert( + offset === (offset | 0), + "milliseconds offset shouldn't be able to exceed int32_t range" + ); + var offsetHours = offset / msPerHour; + var offsetHoursFraction = offset % msPerHour; + if (offsetHoursFraction === 0) { + // Etc/GMT + offset uses POSIX-style signs, i.e. a positive offset + // means a location west of GMT. + timeZone = + "Etc/GMT" + (offsetHours < 0 ? "+" : "-") + std_Math_abs(offsetHours); + + // Check if the fallback is valid. + timeZone = intl_IsValidTimeZoneName(timeZone); + } + + // Fallback to "UTC" if everything else fails. + if (timeZone === null) { + timeZone = "UTC"; + } + } + + // Canonicalize the ICU time zone, e.g. change Etc/UTC to UTC. + var defaultTimeZone = CanonicalizeTimeZoneName(timeZone); + + timeZoneCache.defaultTimeZone = defaultTimeZone; + timeZoneCache.icuDefaultTimeZone = icuDefaultTimeZone; + + return defaultTimeZone; +} + +/** + * Initializes an object as a DateTimeFormat. + * + * This method is complicated a moderate bit by its implementing initialization + * as a *lazy* concept. Everything that must happen now, does -- but we defer + * all the work we can until the object is actually used as a DateTimeFormat. + * This later work occurs in |resolveDateTimeFormatInternals|; steps not noted + * here occur there. + * + * Spec: ECMAScript Internationalization API Specification, 12.1.1. + */ +function InitializeDateTimeFormat( + dateTimeFormat, + thisValue, + locales, + options, + mozExtensions +) { + assert( + IsObject(dateTimeFormat), + "InitializeDateTimeFormat called with non-Object" + ); + assert( + intl_GuardToDateTimeFormat(dateTimeFormat) !== null, + "InitializeDateTimeFormat called with non-DateTimeFormat" + ); + + // Lazy DateTimeFormat data has the following structure: + // + // { + // requestedLocales: List of locales, + // + // localeOpt: // *first* opt computed in InitializeDateTimeFormat + // { + // localeMatcher: "lookup" / "best fit", + // + // ca: string matching a Unicode extension type, // optional + // + // nu: string matching a Unicode extension type, // optional + // + // hc: "h11" / "h12" / "h23" / "h24", // optional + // } + // + // timeZone: IANA time zone name, + // + // formatOpt: // *second* opt computed in InitializeDateTimeFormat + // { + // // all the properties/values listed in Table 3 + // // (weekday, era, year, month, day, &c.) + // + // hour12: true / false, // optional + // } + // + // formatMatcher: "basic" / "best fit", + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every DateTimeFormat lazy data object has *all* these properties, + // never a subset of them. + var lazyDateTimeFormatData = std_Object_create(null); + + // Step 1. + var requestedLocales = CanonicalizeLocaleList(locales); + lazyDateTimeFormatData.requestedLocales = requestedLocales; + + // Step 2. + options = ToDateTimeOptions(options, "any", "date"); + + // Compute options that impact interpretation of locale. + // Step 3. + var localeOpt = new_Record(); + lazyDateTimeFormatData.localeOpt = localeOpt; + + // Steps 4-5. + var localeMatcher = GetOption( + options, + "localeMatcher", + "string", + ["lookup", "best fit"], + "best fit" + ); + localeOpt.localeMatcher = localeMatcher; + + var calendar = GetOption(options, "calendar", "string", undefined, undefined); + + if (calendar !== undefined) { + calendar = intl_ValidateAndCanonicalizeUnicodeExtensionType( + calendar, + "calendar", + "ca" + ); + } + + localeOpt.ca = calendar; + + var numberingSystem = GetOption( + options, + "numberingSystem", + "string", + undefined, + undefined + ); + + if (numberingSystem !== undefined) { + numberingSystem = intl_ValidateAndCanonicalizeUnicodeExtensionType( + numberingSystem, + "numberingSystem", + "nu" + ); + } + + localeOpt.nu = numberingSystem; + + // Step 6. + var hr12 = GetOption(options, "hour12", "boolean", undefined, undefined); + + // Step 7. + var hc = GetOption( + options, + "hourCycle", + "string", + ["h11", "h12", "h23", "h24"], + undefined + ); + + // Step 8. + if (hr12 !== undefined) { + // The "hourCycle" option is ignored if "hr12" is also present. + hc = null; + } + + // Step 9. + localeOpt.hc = hc; + + // Steps 10-16 (see resolveDateTimeFormatInternals). + + // Steps 17-20. + var tz = options.timeZone; + if (tz !== undefined) { + // Step 18.a. + tz = ToString(tz); + + // Step 18.b. + var timeZone = intl_IsValidTimeZoneName(tz); + if (timeZone === null) { + ThrowRangeError(JSMSG_INVALID_TIME_ZONE, tz); + } + + // Step 18.c. + tz = CanonicalizeTimeZoneName(timeZone); + } else { + // Step 19. + tz = DefaultTimeZone(); + } + lazyDateTimeFormatData.timeZone = tz; + + // Step 21. + var formatOpt = new_Record(); + lazyDateTimeFormatData.formatOpt = formatOpt; + + if (mozExtensions) { + let pattern = GetOption(options, "pattern", "string", undefined, undefined); + lazyDateTimeFormatData.patternOption = pattern; + } + + // Step 22. + // 12.1, Table 5: Components of date and time formats. + formatOpt.weekday = GetOption( + options, + "weekday", + "string", + ["narrow", "short", "long"], + undefined + ); + formatOpt.era = GetOption( + options, + "era", + "string", + ["narrow", "short", "long"], + undefined + ); + formatOpt.year = GetOption( + options, + "year", + "string", + ["2-digit", "numeric"], + undefined + ); + formatOpt.month = GetOption( + options, + "month", + "string", + ["2-digit", "numeric", "narrow", "short", "long"], + undefined + ); + formatOpt.day = GetOption( + options, + "day", + "string", + ["2-digit", "numeric"], + undefined + ); + formatOpt.dayPeriod = GetOption( + options, + "dayPeriod", + "string", + ["narrow", "short", "long"], + undefined + ); + formatOpt.hour = GetOption( + options, + "hour", + "string", + ["2-digit", "numeric"], + undefined + ); + formatOpt.minute = GetOption( + options, + "minute", + "string", + ["2-digit", "numeric"], + undefined + ); + formatOpt.second = GetOption( + options, + "second", + "string", + ["2-digit", "numeric"], + undefined + ); + formatOpt.fractionalSecondDigits = GetNumberOption( + options, + "fractionalSecondDigits", + 1, + 3, + undefined + ); + formatOpt.timeZoneName = GetOption( + options, + "timeZoneName", + "string", + [ + "short", + "long", + "shortOffset", + "longOffset", + "shortGeneric", + "longGeneric", + ], + undefined + ); + + // Steps 23-24 provided by ICU - see comment after this function. + + // Step 25. + // + // For some reason (ICU not exposing enough interface?) we drop the + // requested format matcher on the floor after this. In any case, even if + // doing so is justified, we have to do this work here in case it triggers + // getters or similar. (bug 852837) + var formatMatcher = GetOption( + options, + "formatMatcher", + "string", + ["basic", "best fit"], + "best fit" + ); + void formatMatcher; + + // "DateTimeFormat dateStyle & timeStyle" propsal + // https://github.com/tc39/proposal-intl-datetime-style + var dateStyle = GetOption( + options, + "dateStyle", + "string", + ["full", "long", "medium", "short"], + undefined + ); + lazyDateTimeFormatData.dateStyle = dateStyle; + + var timeStyle = GetOption( + options, + "timeStyle", + "string", + ["full", "long", "medium", "short"], + undefined + ); + lazyDateTimeFormatData.timeStyle = timeStyle; + + if (dateStyle !== undefined || timeStyle !== undefined) { + var optionsList = [ + "weekday", + "era", + "year", + "month", + "day", + "dayPeriod", + "hour", + "minute", + "second", + "fractionalSecondDigits", + "timeZoneName", + ]; + + for (var i = 0; i < optionsList.length; i++) { + var option = optionsList[i]; + if (formatOpt[option] !== undefined) { + ThrowTypeError( + JSMSG_INVALID_DATETIME_OPTION, + option, + dateStyle !== undefined ? "dateStyle" : "timeStyle" + ); + } + } + } + + // Steps 26-28 provided by ICU, more or less - see comment after this function. + + // Steps 29-30. + // Pass hr12 on to ICU. + if (hr12 !== undefined) { + formatOpt.hour12 = hr12; + } + + // Step 32. + // + // We've done everything that must be done now: mark the lazy data as fully + // computed and install it. + initializeIntlObject( + dateTimeFormat, + "DateTimeFormat", + lazyDateTimeFormatData + ); + + // 12.2.1, steps 4-5. + if ( + dateTimeFormat !== thisValue && + callFunction( + std_Object_isPrototypeOf, + GetBuiltinPrototype("DateTimeFormat"), + thisValue + ) + ) { + DefineDataProperty( + thisValue, + intlFallbackSymbol(), + dateTimeFormat, + ATTR_NONENUMERABLE | ATTR_NONCONFIGURABLE | ATTR_NONWRITABLE + ); + + return thisValue; + } + + // 12.2.1, step 6. + return dateTimeFormat; +} + +/** + * Returns a new options object that includes the provided options (if any) + * and fills in default components if required components are not defined. + * Required can be "date", "time", or "any". + * Defaults can be "date", "time", or "all". + * + * Spec: ECMAScript Internationalization API Specification, 12.1.1. + */ +function ToDateTimeOptions(options, required, defaults) { + assert(typeof required === "string", "ToDateTimeOptions"); + assert(typeof defaults === "string", "ToDateTimeOptions"); + + // Steps 1-2. + if (options === undefined) { + options = null; + } else { + options = ToObject(options); + } + options = std_Object_create(options); + + // Step 3. + var needDefaults = true; + + // Step 4. + if (required === "date" || required === "any") { + if (options.weekday !== undefined) { + needDefaults = false; + } + if (options.year !== undefined) { + needDefaults = false; + } + if (options.month !== undefined) { + needDefaults = false; + } + if (options.day !== undefined) { + needDefaults = false; + } + } + + // Step 5. + if (required === "time" || required === "any") { + if (options.dayPeriod !== undefined) { + needDefaults = false; + } + if (options.hour !== undefined) { + needDefaults = false; + } + if (options.minute !== undefined) { + needDefaults = false; + } + if (options.second !== undefined) { + needDefaults = false; + } + if (options.fractionalSecondDigits !== undefined) { + needDefaults = false; + } + } + + // "DateTimeFormat dateStyle & timeStyle" propsal + // https://github.com/tc39/proposal-intl-datetime-style + var dateStyle = options.dateStyle; + var timeStyle = options.timeStyle; + + if (dateStyle !== undefined || timeStyle !== undefined) { + needDefaults = false; + } + + if (required === "date" && timeStyle !== undefined) { + ThrowTypeError( + JSMSG_INVALID_DATETIME_STYLE, + "timeStyle", + "toLocaleDateString" + ); + } + + if (required === "time" && dateStyle !== undefined) { + ThrowTypeError( + JSMSG_INVALID_DATETIME_STYLE, + "dateStyle", + "toLocaleTimeString" + ); + } + + // Step 6. + if (needDefaults && (defaults === "date" || defaults === "all")) { + // The specification says to call [[DefineOwnProperty]] with false for + // the Throw parameter, while Object.defineProperty uses true. For the + // calls here, the difference doesn't matter because we're adding + // properties to a new object. + DefineDataProperty(options, "year", "numeric"); + DefineDataProperty(options, "month", "numeric"); + DefineDataProperty(options, "day", "numeric"); + } + + // Step 7. + if (needDefaults && (defaults === "time" || defaults === "all")) { + // See comment for step 7. + DefineDataProperty(options, "hour", "numeric"); + DefineDataProperty(options, "minute", "numeric"); + DefineDataProperty(options, "second", "numeric"); + } + + // Step 8. + return options; +} + +/** + * Returns the subset of the given locale list for which this locale list has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript Internationalization API Specification, 12.3.2. + */ +function Intl_DateTimeFormat_supportedLocalesOf(locales /*, options*/) { + var options = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 1. + var availableLocales = "DateTimeFormat"; + + // Step 2. + var requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. + return SupportedLocales(availableLocales, requestedLocales, options); +} + +/** + * DateTimeFormat internal properties. + * + * Spec: ECMAScript Internationalization API Specification, 9.1 and 12.3.3. + */ +var dateTimeFormatInternalProperties = { + localeData: dateTimeFormatLocaleData, + relevantExtensionKeys: ["ca", "hc", "nu"], +}; + +function dateTimeFormatLocaleData() { + return { + ca: intl_availableCalendars, + nu: getNumberingSystems, + hc: () => { + return [null, "h11", "h12", "h23", "h24"]; + }, + default: { + ca: intl_defaultCalendar, + nu: intl_numberingSystem, + hc: () => { + return null; + }, + }, + }; +} + +/** + * Create function to be cached and returned by Intl.DateTimeFormat.prototype.format. + * + * Spec: ECMAScript Internationalization API Specification, 12.1.5. + */ +function createDateTimeFormatFormat(dtf) { + // This function is not inlined in $Intl_DateTimeFormat_format_get to avoid + // creating a call-object on each call to $Intl_DateTimeFormat_format_get. + return function(date) { + // Step 1 (implicit). + + // Step 2. + assert(IsObject(dtf), "dateTimeFormatFormatToBind called with non-Object"); + assert( + intl_GuardToDateTimeFormat(dtf) !== null, + "dateTimeFormatFormatToBind called with non-DateTimeFormat" + ); + + // Steps 3-4. + var x = date === undefined ? std_Date_now() : ToNumber(date); + + // Step 5. + return intl_FormatDateTime(dtf, x, /* formatToParts = */ false); + }; +} + +/** + * Returns a function bound to this DateTimeFormat that returns a String value + * representing the result of calling ToNumber(date) according to the + * effective locale and the formatting options of this DateTimeFormat. + * + * Spec: ECMAScript Internationalization API Specification, 12.4.3. + */ +// Uncloned functions with `$` prefix are allocated as extended function +// to store the original name in `SetCanonicalName`. +function $Intl_DateTimeFormat_format_get() { + // Steps 1-3. + var thisArg = UnwrapDateTimeFormat(this); + var dtf = thisArg; + if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) { + return callFunction( + intl_CallDateTimeFormatMethodIfWrapped, + thisArg, + "$Intl_DateTimeFormat_format_get" + ); + } + + var internals = getDateTimeFormatInternals(dtf); + + // Step 4. + if (internals.boundFormat === undefined) { + // Steps 4.a-c. + internals.boundFormat = createDateTimeFormatFormat(dtf); + } + + // Step 5. + return internals.boundFormat; +} +SetCanonicalName($Intl_DateTimeFormat_format_get, "get format"); + +/** + * Intl.DateTimeFormat.prototype.formatToParts ( date ) + * + * Spec: ECMAScript Internationalization API Specification, 12.4.4. + */ +function Intl_DateTimeFormat_formatToParts(date) { + // Step 1. + var dtf = this; + + // Steps 2-3. + if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) { + return callFunction( + intl_CallDateTimeFormatMethodIfWrapped, + this, + date, + "Intl_DateTimeFormat_formatToParts" + ); + } + + // Steps 4-5. + var x = date === undefined ? std_Date_now() : ToNumber(date); + + // Ensure the DateTimeFormat internals are resolved. + getDateTimeFormatInternals(dtf); + + // Step 6. + return intl_FormatDateTime(dtf, x, /* formatToParts = */ true); +} + +/** + * Intl.DateTimeFormat.prototype.formatRange ( startDate , endDate ) + * + * Spec: Intl.DateTimeFormat.prototype.formatRange proposal + */ +function Intl_DateTimeFormat_formatRange(startDate, endDate) { + // Step 1. + var dtf = this; + + // Step 2. + if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) { + return callFunction( + intl_CallDateTimeFormatMethodIfWrapped, + this, + startDate, + endDate, + "Intl_DateTimeFormat_formatRange" + ); + } + + // Step 3. + if (startDate === undefined || endDate === undefined) { + ThrowTypeError( + JSMSG_UNDEFINED_DATE, + startDate === undefined ? "start" : "end", + "formatRange" + ); + } + + // Step 4. + var x = ToNumber(startDate); + + // Step 5. + var y = ToNumber(endDate); + + // Ensure the DateTimeFormat internals are resolved. + getDateTimeFormatInternals(dtf); + + // Step 6. + return intl_FormatDateTimeRange(dtf, x, y, /* formatToParts = */ false); +} + +/** + * Intl.DateTimeFormat.prototype.formatRangeToParts ( startDate , endDate ) + * + * Spec: Intl.DateTimeFormat.prototype.formatRange proposal + */ +function Intl_DateTimeFormat_formatRangeToParts(startDate, endDate) { + // Step 1. + var dtf = this; + + // Step 2. + if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) { + return callFunction( + intl_CallDateTimeFormatMethodIfWrapped, + this, + startDate, + endDate, + "Intl_DateTimeFormat_formatRangeToParts" + ); + } + + // Step 3. + if (startDate === undefined || endDate === undefined) { + ThrowTypeError( + JSMSG_UNDEFINED_DATE, + startDate === undefined ? "start" : "end", + "formatRangeToParts" + ); + } + + // Step 4. + var x = ToNumber(startDate); + + // Step 5. + var y = ToNumber(endDate); + + // Ensure the DateTimeFormat internals are resolved. + getDateTimeFormatInternals(dtf); + + // Step 6. + return intl_FormatDateTimeRange(dtf, x, y, /* formatToParts = */ true); +} + +/** + * Returns the resolved options for a DateTimeFormat object. + * + * Spec: ECMAScript Internationalization API Specification, 12.4.5. + */ +function Intl_DateTimeFormat_resolvedOptions() { + // Steps 1-3. + var thisArg = UnwrapDateTimeFormat(this); + var dtf = thisArg; + if (!IsObject(dtf) || (dtf = intl_GuardToDateTimeFormat(dtf)) === null) { + return callFunction( + intl_CallDateTimeFormatMethodIfWrapped, + thisArg, + "Intl_DateTimeFormat_resolvedOptions" + ); + } + + // Ensure the internals are resolved. + var internals = getDateTimeFormatInternals(dtf); + + // Steps 4-5. + var result = { + locale: internals.locale, + calendar: internals.calendar, + numberingSystem: internals.numberingSystem, + timeZone: internals.timeZone, + }; + + if (internals.pattern !== undefined) { + // The raw pattern option is only internal to Mozilla, and not part of the + // ECMA-402 API. + DefineDataProperty(result, "pattern", internals.pattern); + } + + var hasDateStyle = internals.dateStyle !== undefined; + var hasTimeStyle = internals.timeStyle !== undefined; + + if (hasDateStyle || hasTimeStyle) { + if (hasTimeStyle) { + // timeStyle (unlike dateStyle) requires resolving the pattern to + // ensure "hourCycle" and "hour12" properties are added to |result|. + intl_resolveDateTimeFormatComponents( + dtf, + result, + /* includeDateTimeFields = */ false + ); + } + if (hasDateStyle) { + DefineDataProperty(result, "dateStyle", internals.dateStyle); + } + if (hasTimeStyle) { + DefineDataProperty(result, "timeStyle", internals.timeStyle); + } + } else { + // Components bag or a (Mozilla-only) raw pattern. + intl_resolveDateTimeFormatComponents( + dtf, + result, + /* includeDateTimeFields = */ true + ); + } + + // Step 6. + return result; +} diff --git a/js/src/builtin/intl/DecimalNumber.cpp b/js/src/builtin/intl/DecimalNumber.cpp new file mode 100644 index 0000000000..ef83edbb87 --- /dev/null +++ b/js/src/builtin/intl/DecimalNumber.cpp @@ -0,0 +1,263 @@ +/* -*- 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 "builtin/intl/DecimalNumber.h" + +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/TextUtils.h" + +#include "js/GCAPI.h" +#include "util/Text.h" +#include "util/Unicode.h" +#include "vm/StringType.h" + +int32_t js::intl::DecimalNumber::compareTo(const DecimalNumber& other) const { + // Can't compare if the exponent is too large. + MOZ_ASSERT(!exponentTooLarge()); + MOZ_ASSERT(!other.exponentTooLarge()); + + // If the signs don't match, the negative number is smaller. + if (isNegative() != other.isNegative()) { + return isNegative() ? -1 : 1; + } + + // Next handle the case when one of the numbers is zero. + if (isZero()) { + return other.isZero() ? 0 : other.isNegative() ? 1 : -1; + } + if (other.isZero()) { + return isNegative() ? -1 : 1; + } + + // If the exponent is different, the number with the smaller exponent is + // smaller in total, unless the numbers are negative. + if (exponent() != other.exponent()) { + return (exponent() < other.exponent() ? -1 : 1) * (isNegative() ? -1 : 1); + } + + class Significand { + const DecimalNumber& decimal_; + size_t index_; + + public: + explicit Significand(const DecimalNumber& decimal) : decimal_(decimal) { + index_ = decimal.significandStart_; + } + + int32_t next() { + // Any remaining digits in the significand are implicit zeros. + if (index_ >= decimal_.significandEnd_) { + return 0; + } + + char ch = decimal_.charAt(index_++); + + // Skip over the decimal point. + if (ch == '.') { + if (index_ >= decimal_.significandEnd_) { + return 0; + } + ch = decimal_.charAt(index_++); + } + + MOZ_ASSERT(mozilla::IsAsciiDigit(ch)); + return AsciiDigitToNumber(ch); + } + }; + + // Both numbers have the same sign, neither of them is zero, and they have the + // same exponent. Next compare the significand digit by digit until we find + // the first difference. + + Significand s1(*this); + Significand s2(other); + for (int32_t e = std::abs(exponent()); e >= 0; e--) { + int32_t x = s1.next(); + int32_t y = s2.next(); + if (int32_t r = x - y) { + return r * (isNegative() ? -1 : 1); + } + } + + // No different significand digit was found, so the numbers are equal. + return 0; +} + +mozilla::Maybe<js::intl::DecimalNumber> js::intl::DecimalNumber::from( + JSLinearString* str, JS::AutoCheckCannotGC& nogc) { + return str->hasLatin1Chars() ? from<Latin1Char>(str->latin1Range(nogc)) + : from<char16_t>(str->twoByteRange(nogc)); +} + +template <typename CharT> +mozilla::Maybe<js::intl::DecimalNumber> js::intl::DecimalNumber::from( + mozilla::Span<const CharT> chars) { + // This algorithm matches a subset of the `StringNumericLiteral` grammar + // production of ECMAScript. In particular, we do *not* allow: + // - NonDecimalIntegerLiteral (eg. "0x10") + // - NumericLiteralSeparator (eg. "123_456") + // - Infinity (eg. "-Infinity") + + DecimalNumber number(chars); + + // Skip over leading whitespace. + size_t i = 0; + while (i < chars.size() && unicode::IsSpace(chars[i])) { + i++; + } + + // The number is only whitespace, treat as zero. + if (i == chars.size()) { + number.zero_ = true; + return mozilla::Some(number); + } + + // Read the optional sign. + if (auto ch = chars[i]; ch == '-' || ch == '+') { + i++; + number.negative_ = ch == '-'; + + if (i == chars.size()) { + return mozilla::Nothing(); + } + } + + // Must start with either a digit or the decimal point. + size_t startInteger = i; + size_t endInteger = i; + if (auto ch = chars[i]; mozilla::IsAsciiDigit(ch)) { + // Skip over leading zeros. + while (i < chars.size() && chars[i] == '0') { + i++; + } + + // Read the integer part. + startInteger = i; + while (i < chars.size() && mozilla::IsAsciiDigit(chars[i])) { + i++; + } + endInteger = i; + } else if (ch == '.') { + // There must be a digit when the number starts with the decimal point. + if (i + 1 == chars.size() || !mozilla::IsAsciiDigit(chars[i + 1])) { + return mozilla::Nothing(); + } + } else { + return mozilla::Nothing(); + } + + // Read the fractional part. + size_t startFraction = i; + size_t endFraction = i; + if (i < chars.size() && chars[i] == '.') { + i++; + + startFraction = i; + while (i < chars.size() && mozilla::IsAsciiDigit(chars[i])) { + i++; + } + endFraction = i; + + // Ignore trailing zeros in the fractional part. + while (startFraction <= endFraction && chars[endFraction - 1] == '0') { + endFraction--; + } + } + + // Read the exponent. + if (i < chars.size() && (chars[i] == 'e' || chars[i] == 'E')) { + i++; + + if (i == chars.size()) { + return mozilla::Nothing(); + } + + int32_t exponentSign = 1; + if (auto ch = chars[i]; ch == '-' || ch == '+') { + i++; + exponentSign = ch == '-' ? -1 : +1; + + if (i == chars.size()) { + return mozilla::Nothing(); + } + } + + if (!mozilla::IsAsciiDigit(chars[i])) { + return mozilla::Nothing(); + } + + mozilla::CheckedInt32 exp = 0; + while (i < chars.size() && mozilla::IsAsciiDigit(chars[i])) { + exp *= 10; + exp += AsciiDigitToNumber(chars[i]); + + i++; + } + + // Check for exponent overflow. + if (exp.isValid()) { + number.exponent_ = exp.value() * exponentSign; + } else { + number.exponentTooLarge_ = true; + } + } + + // Skip over trailing whitespace. + while (i < chars.size() && unicode::IsSpace(chars[i])) { + i++; + } + + // The complete string must have been parsed. + if (i != chars.size()) { + return mozilla::Nothing(); + } + + if (startInteger < endInteger) { + // We have a non-zero integer part. + + mozilla::CheckedInt32 integerExponent = number.exponent_; + integerExponent += size_t(endInteger - startInteger); + + if (integerExponent.isValid()) { + number.exponent_ = integerExponent.value(); + } else { + number.exponent_ = 0; + number.exponentTooLarge_ = true; + } + + number.significandStart_ = startInteger; + number.significandEnd_ = endFraction; + } else if (startFraction < endFraction) { + // We have a non-zero fractional part. + + // Skip over leading zeros + size_t i = startFraction; + while (i < endFraction && chars[i] == '0') { + i++; + } + + mozilla::CheckedInt32 fractionExponent = number.exponent_; + fractionExponent -= size_t(i - startFraction); + + if (fractionExponent.isValid() && fractionExponent.value() != INT32_MIN) { + number.exponent_ = fractionExponent.value(); + } else { + number.exponent_ = 0; + number.exponentTooLarge_ = true; + } + + number.significandStart_ = i; + number.significandEnd_ = endFraction; + } else { + // The number is zero, clear the error flag if it was set. + number.zero_ = true; + number.exponent_ = 0; + number.exponentTooLarge_ = false; + } + + return mozilla::Some(number); +} diff --git a/js/src/builtin/intl/DecimalNumber.h b/js/src/builtin/intl/DecimalNumber.h new file mode 100644 index 0000000000..2373ae0f7f --- /dev/null +++ b/js/src/builtin/intl/DecimalNumber.h @@ -0,0 +1,117 @@ +/* -*- 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 builtin_intl_DecimalNumber_h +#define builtin_intl_DecimalNumber_h + +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/Span.h" +#include "mozilla/Variant.h" + +#include <stddef.h> +#include <stdint.h> + +#include "jstypes.h" + +#include "js/TypeDecls.h" + +class JSLinearString; + +namespace JS { +class JS_PUBLIC_API AutoCheckCannotGC; +} + +namespace js::intl { + +/** + * Representation of a decimal number in normalized form. + * + * Examples of normalized forms: + * - "123" is normalized to "0.123e3". + * - "0.01e-4" is normalized to "0.1e-5". + * - "12.3" is normalized to "0.123e2". + * + * Note: Internally we leave the decimal point where it lies to avoid copying + * the string, but otherwise ignore it once we calculate the normalized + * exponent. + * + * TODO: Remove unused capabilities once there's a concrete PR for + * <https://github.com/tc39/proposal-intl-numberformat-v3/issues/98>. + */ +class MOZ_STACK_CLASS DecimalNumber final { + using Latin1String = mozilla::Span<const JS::Latin1Char>; + using TwoByteString = mozilla::Span<const char16_t>; + + mozilla::Variant<Latin1String, TwoByteString> string_; + + char charAt(size_t i) const { + if (string_.is<Latin1String>()) { + return static_cast<char>(string_.as<Latin1String>()[i]); + } + return static_cast<char>(string_.as<TwoByteString>()[i]); + } + + // Decimal exponent. Valid range is (INT32_MIN, INT_MAX32]. + int32_t exponent_ = 0; + + // Start and end position of the significand. + size_t significandStart_ = 0; + size_t significandEnd_ = 0; + + // Flag if the number is zero. + bool zero_ = false; + + // Flag for negative numbers. + bool negative_ = false; + + // Error flag when the exponent is too large. + bool exponentTooLarge_ = false; + + template <typename CharT> + explicit DecimalNumber(mozilla::Span<const CharT> string) : string_(string) {} + + public: + /** Return true if this decimal is zero. */ + bool isZero() const { return zero_; } + + /** Return true if this decimal is negative. */ + bool isNegative() const { return negative_; } + + /** Return true if the exponent is too large. */ + bool exponentTooLarge() const { return exponentTooLarge_; } + + /** Return the exponent of this decimal. */ + int32_t exponent() const { return exponent_; } + + // Exposed for testing. + size_t significandStart() const { return significandStart_; } + size_t significandEnd() const { return significandEnd_; } + + /** + * Compare this decimal to another decimal. Returns a negative value if this + * decimal is smaller; zero if this decimal is equal; or a positive value if + * this decimal is larger than the input. + */ + int32_t compareTo(const DecimalNumber& other) const; + + /** + * Create a decimal number from the input. Returns |mozilla::Nothing| if the + * input can't be parsed. + */ + template <typename CharT> + static mozilla::Maybe<DecimalNumber> from(mozilla::Span<const CharT> chars); + + /** + * Create a decimal number from the input. Returns |mozilla::Nothing| if the + * input can't be parsed. + */ + static mozilla::Maybe<DecimalNumber> from(JSLinearString* str, + JS::AutoCheckCannotGC& nogc); +}; +} // namespace js::intl + +#endif /* builtin_intl_DecimalNumber_h */ diff --git a/js/src/builtin/intl/DisplayNames.cpp b/js/src/builtin/intl/DisplayNames.cpp new file mode 100644 index 0000000000..7d44c31a2f --- /dev/null +++ b/js/src/builtin/intl/DisplayNames.cpp @@ -0,0 +1,551 @@ +/* -*- 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/. */ + +/* Intl.DisplayNames implementation. */ + +#include "builtin/intl/DisplayNames.h" + +#include "mozilla/Assertions.h" +#include "mozilla/intl/DisplayNames.h" +#include "mozilla/PodOperations.h" +#include "mozilla/Span.h" + +#include <algorithm> + +#include "jsnum.h" +#include "jspubtd.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/FormatBuffer.h" +#include "gc/AllocKind.h" +#include "gc/GCContext.h" +#include "js/CallArgs.h" +#include "js/Class.h" +#include "js/experimental/Intl.h" // JS::AddMozDisplayNamesConstructor +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/Printer.h" +#include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperties +#include "js/PropertyDescriptor.h" +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Utility.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/Runtime.h" +#include "vm/SelfHosting.h" +#include "vm/Stack.h" +#include "vm/StringType.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +const JSClassOps DisplayNamesObject::classOps_ = {nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* enumerate */ + nullptr, /* newEnumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + DisplayNamesObject::finalize}; + +const JSClass DisplayNamesObject::class_ = { + "Intl.DisplayNames", + JSCLASS_HAS_RESERVED_SLOTS(DisplayNamesObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_DisplayNames) | + JSCLASS_FOREGROUND_FINALIZE, + &DisplayNamesObject::classOps_, &DisplayNamesObject::classSpec_}; + +const JSClass& DisplayNamesObject::protoClass_ = PlainObject::class_; + +static bool displayNames_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().DisplayNames); + return true; +} + +static const JSFunctionSpec displayNames_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", + "Intl_DisplayNames_supportedLocalesOf", 1, 0), + JS_FS_END}; + +static const JSFunctionSpec displayNames_methods[] = { + JS_SELF_HOSTED_FN("of", "Intl_DisplayNames_of", 1, 0), + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DisplayNames_resolvedOptions", 0, + 0), + JS_FN(js_toSource_str, displayNames_toSource, 0, 0), JS_FS_END}; + +static const JSPropertySpec displayNames_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Intl.DisplayNames", JSPROP_READONLY), + JS_PS_END}; + +static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp); + +const ClassSpec DisplayNamesObject::classSpec_ = { + GenericCreateConstructor<DisplayNames, 2, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<DisplayNamesObject>, + displayNames_static_methods, + nullptr, + displayNames_methods, + displayNames_properties, + nullptr, + ClassSpec::DontDefineConstructor}; + +enum class DisplayNamesOptions { + Standard, + + // Calendar display names are no longer available with the current spec + // proposal text, but may be re-enabled in the future. For our internal use + // we still need to have them present, so use a feature guard for now. + EnableMozExtensions, +}; + +/** + * Initialize a new Intl.DisplayNames object using the named self-hosted + * function. + */ +static bool InitializeDisplayNamesObject(JSContext* cx, HandleObject obj, + Handle<PropertyName*> initializer, + HandleValue locales, + HandleValue options, + DisplayNamesOptions dnoptions) { + FixedInvokeArgs<4> args(cx); + + args[0].setObject(*obj); + args[1].set(locales); + args[2].set(options); + args[3].setBoolean(dnoptions == DisplayNamesOptions::EnableMozExtensions); + + RootedValue ignored(cx); + if (!CallSelfHostedFunction(cx, initializer, NullHandleValue, args, + &ignored)) { + return false; + } + + MOZ_ASSERT(ignored.isUndefined(), + "Unexpected return value from non-legacy Intl object initializer"); + return true; +} + +/** + * Intl.DisplayNames ([ locales [ , options ]]) + */ +static bool DisplayNames(JSContext* cx, const CallArgs& args, + DisplayNamesOptions dnoptions) { + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Intl.DisplayNames")) { + return false; + } + + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (dnoptions == DisplayNamesOptions::Standard) { + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_DisplayNames, + &proto)) { + return false; + } + } else { + RootedObject newTarget(cx, &args.newTarget().toObject()); + if (!GetPrototypeFromConstructor(cx, newTarget, JSProto_Null, &proto)) { + return false; + } + } + + Rooted<DisplayNamesObject*> displayNames(cx); + displayNames = NewObjectWithClassProto<DisplayNamesObject>(cx, proto); + if (!displayNames) { + return false; + } + + HandleValue locales = args.get(0); + HandleValue options = args.get(1); + + // Steps 3-26. + if (!InitializeDisplayNamesObject(cx, displayNames, + cx->names().InitializeDisplayNames, locales, + options, dnoptions)) { + return false; + } + + // Step 27. + args.rval().setObject(*displayNames); + return true; +} + +static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return DisplayNames(cx, args, DisplayNamesOptions::Standard); +} + +static bool MozDisplayNames(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return DisplayNames(cx, args, DisplayNamesOptions::EnableMozExtensions); +} + +void js::DisplayNamesObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + + if (mozilla::intl::DisplayNames* displayNames = + obj->as<DisplayNamesObject>().getDisplayNames()) { + intl::RemoveICUCellMemory(gcx, obj, DisplayNamesObject::EstimatedMemoryUse); + delete displayNames; + } +} + +bool JS::AddMozDisplayNamesConstructor(JSContext* cx, HandleObject intl) { + RootedObject ctor(cx, GlobalObject::createConstructor( + cx, MozDisplayNames, cx->names().DisplayNames, 2)); + if (!ctor) { + return false; + } + + RootedObject proto( + cx, GlobalObject::createBlankPrototype<PlainObject>(cx, cx->global())); + if (!proto) { + return false; + } + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) { + return false; + } + + if (!JS_DefineFunctions(cx, ctor, displayNames_static_methods)) { + return false; + } + + if (!JS_DefineFunctions(cx, proto, displayNames_methods)) { + return false; + } + + if (!JS_DefineProperties(cx, proto, displayNames_properties)) { + return false; + } + + RootedValue ctorValue(cx, ObjectValue(*ctor)); + return DefineDataProperty(cx, intl, cx->names().DisplayNames, ctorValue, 0); +} + +static mozilla::intl::DisplayNames* NewDisplayNames( + JSContext* cx, const char* locale, + mozilla::intl::DisplayNames::Options& options) { + auto result = mozilla::intl::DisplayNames::TryCreate(locale, options); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + return result.unwrap().release(); +} + +static mozilla::intl::DisplayNames* GetOrCreateDisplayNames( + JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale, + mozilla::intl::DisplayNames::Options& options) { + // Obtain a cached mozilla::intl::DisplayNames object. + mozilla::intl::DisplayNames* dn = displayNames->getDisplayNames(); + if (!dn) { + dn = NewDisplayNames(cx, locale, options); + if (!dn) { + return nullptr; + } + displayNames->setDisplayNames(dn); + + intl::AddICUCellMemory(displayNames, + DisplayNamesObject::EstimatedMemoryUse); + } + return dn; +} + +static void ReportInvalidOptionError(JSContext* cx, HandleString type, + HandleString option) { + if (UniqueChars optionStr = QuoteString(cx, option, '"')) { + if (UniqueChars typeStr = QuoteString(cx, type)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, typeStr.get(), + optionStr.get()); + } + } +} + +static void ReportInvalidOptionError(JSContext* cx, const char* type, + HandleString option) { + if (UniqueChars str = QuoteString(cx, option, '"')) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, type, str.get()); + } +} + +static void ReportInvalidOptionError(JSContext* cx, const char* type, + double option) { + ToCStringBuf cbuf; + const char* str = NumberToCString(&cbuf, option); + MOZ_ASSERT(str); + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_DIGITS_VALUE, str); +} + +/** + * intl_ComputeDisplayName(displayNames, locale, calendar, style, + * languageDisplay, fallback, type, code) + */ +bool js::intl_ComputeDisplayName(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 8); + + Rooted<DisplayNamesObject*> displayNames( + cx, &args[0].toObject().as<DisplayNamesObject>()); + + UniqueChars locale = intl::EncodeLocale(cx, args[1].toString()); + if (!locale) { + return false; + } + + Rooted<JSLinearString*> calendar(cx, args[2].toString()->ensureLinear(cx)); + if (!calendar) { + return false; + } + + Rooted<JSLinearString*> code(cx, args[7].toString()->ensureLinear(cx)); + if (!code) { + return false; + } + + mozilla::intl::DisplayNames::Style style; + { + JSLinearString* styleStr = args[3].toString()->ensureLinear(cx); + if (!styleStr) { + return false; + } + + if (StringEqualsLiteral(styleStr, "long")) { + style = mozilla::intl::DisplayNames::Style::Long; + } else if (StringEqualsLiteral(styleStr, "short")) { + style = mozilla::intl::DisplayNames::Style::Short; + } else if (StringEqualsLiteral(styleStr, "narrow")) { + style = mozilla::intl::DisplayNames::Style::Narrow; + } else { + MOZ_ASSERT(StringEqualsLiteral(styleStr, "abbreviated")); + style = mozilla::intl::DisplayNames::Style::Abbreviated; + } + } + + mozilla::intl::DisplayNames::LanguageDisplay languageDisplay; + { + JSLinearString* language = args[4].toString()->ensureLinear(cx); + if (!language) { + return false; + } + + if (StringEqualsLiteral(language, "dialect")) { + languageDisplay = mozilla::intl::DisplayNames::LanguageDisplay::Dialect; + } else { + MOZ_ASSERT(language->empty() || + StringEqualsLiteral(language, "standard")); + languageDisplay = mozilla::intl::DisplayNames::LanguageDisplay::Standard; + } + } + + mozilla::intl::DisplayNames::Fallback fallback; + { + JSLinearString* fallbackStr = args[5].toString()->ensureLinear(cx); + if (!fallbackStr) { + return false; + } + + if (StringEqualsLiteral(fallbackStr, "none")) { + fallback = mozilla::intl::DisplayNames::Fallback::None; + } else { + MOZ_ASSERT(StringEqualsLiteral(fallbackStr, "code")); + fallback = mozilla::intl::DisplayNames::Fallback::Code; + } + } + + Rooted<JSLinearString*> type(cx, args[6].toString()->ensureLinear(cx)); + if (!type) { + return false; + } + + mozilla::intl::DisplayNames::Options options{ + style, + languageDisplay, + }; + + // If a calendar exists, set it as an option. + JS::UniqueChars calendarChars = nullptr; + if (!calendar->empty()) { + calendarChars = JS_EncodeStringToUTF8(cx, calendar); + if (!calendarChars) { + return false; + } + } + + mozilla::intl::DisplayNames* dn = + GetOrCreateDisplayNames(cx, displayNames, locale.get(), options); + if (!dn) { + return false; + } + + // The "code" is usually a small ASCII string, so try to avoid an allocation + // by copying it to the stack. Unfortunately we can't pass a string span of + // the JSString directly to the unified DisplayNames API, as the + // intl::FormatBuffer will be written to. This writing can trigger a GC and + // invalidate the span, creating a nogc rooting hazard. + JS::UniqueChars utf8 = nullptr; + unsigned char ascii[32]; + mozilla::Span<const char> codeSpan = nullptr; + if (code->length() < 32 && code->hasLatin1Chars() && StringIsAscii(code)) { + JS::AutoCheckCannotGC nogc; + mozilla::PodCopy(ascii, code->latin1Chars(nogc), code->length()); + codeSpan = + mozilla::Span(reinterpret_cast<const char*>(ascii), code->length()); + } else { + utf8 = JS_EncodeStringToUTF8(cx, code); + if (!utf8) { + return false; + } + codeSpan = mozilla::MakeStringSpan(utf8.get()); + } + + intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + mozilla::Result<mozilla::Ok, mozilla::intl::DisplayNamesError> result = + mozilla::Ok{}; + + if (StringEqualsLiteral(type, "language")) { + result = dn->GetLanguage(buffer, codeSpan, fallback); + } else if (StringEqualsLiteral(type, "script")) { + result = dn->GetScript(buffer, codeSpan, fallback); + } else if (StringEqualsLiteral(type, "region")) { + result = dn->GetRegion(buffer, codeSpan, fallback); + } else if (StringEqualsLiteral(type, "currency")) { + result = dn->GetCurrency(buffer, codeSpan, fallback); + } else if (StringEqualsLiteral(type, "calendar")) { + result = dn->GetCalendar(buffer, codeSpan, fallback); + } else if (StringEqualsLiteral(type, "weekday")) { + double d = LinearStringToNumber(code); + if (!IsInteger(d) || d < 1 || d > 7) { + ReportInvalidOptionError(cx, "weekday", d); + return false; + } + result = + dn->GetWeekday(buffer, static_cast<mozilla::intl::Weekday>(d), + mozilla::MakeStringSpan(calendarChars.get()), fallback); + } else if (StringEqualsLiteral(type, "month")) { + double d = LinearStringToNumber(code); + if (!IsInteger(d) || d < 1 || d > 13) { + ReportInvalidOptionError(cx, "month", d); + return false; + } + + result = + dn->GetMonth(buffer, static_cast<mozilla::intl::Month>(d), + mozilla::MakeStringSpan(calendarChars.get()), fallback); + + } else if (StringEqualsLiteral(type, "quarter")) { + double d = LinearStringToNumber(code); + + // Inlined implementation of `IsValidQuarterCode ( quarter )`. + if (!IsInteger(d) || d < 1 || d > 4) { + ReportInvalidOptionError(cx, "quarter", d); + return false; + } + + result = + dn->GetQuarter(buffer, static_cast<mozilla::intl::Quarter>(d), + mozilla::MakeStringSpan(calendarChars.get()), fallback); + + } else if (StringEqualsLiteral(type, "dayPeriod")) { + mozilla::intl::DayPeriod dayPeriod; + if (StringEqualsLiteral(code, "am")) { + dayPeriod = mozilla::intl::DayPeriod::AM; + } else if (StringEqualsLiteral(code, "pm")) { + dayPeriod = mozilla::intl::DayPeriod::PM; + } else { + ReportInvalidOptionError(cx, "dayPeriod", code); + return false; + } + result = dn->GetDayPeriod(buffer, dayPeriod, + mozilla::MakeStringSpan(calendarChars.get()), + fallback); + + } else { + MOZ_ASSERT(StringEqualsLiteral(type, "dateTimeField")); + mozilla::intl::DateTimeField field; + if (StringEqualsLiteral(code, "era")) { + field = mozilla::intl::DateTimeField::Era; + } else if (StringEqualsLiteral(code, "year")) { + field = mozilla::intl::DateTimeField::Year; + } else if (StringEqualsLiteral(code, "quarter")) { + field = mozilla::intl::DateTimeField::Quarter; + } else if (StringEqualsLiteral(code, "month")) { + field = mozilla::intl::DateTimeField::Month; + } else if (StringEqualsLiteral(code, "weekOfYear")) { + field = mozilla::intl::DateTimeField::WeekOfYear; + } else if (StringEqualsLiteral(code, "weekday")) { + field = mozilla::intl::DateTimeField::Weekday; + } else if (StringEqualsLiteral(code, "day")) { + field = mozilla::intl::DateTimeField::Day; + } else if (StringEqualsLiteral(code, "dayPeriod")) { + field = mozilla::intl::DateTimeField::DayPeriod; + } else if (StringEqualsLiteral(code, "hour")) { + field = mozilla::intl::DateTimeField::Hour; + } else if (StringEqualsLiteral(code, "minute")) { + field = mozilla::intl::DateTimeField::Minute; + } else if (StringEqualsLiteral(code, "second")) { + field = mozilla::intl::DateTimeField::Second; + } else if (StringEqualsLiteral(code, "timeZoneName")) { + field = mozilla::intl::DateTimeField::TimeZoneName; + } else { + ReportInvalidOptionError(cx, "dateTimeField", code); + return false; + } + + intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + mozilla::intl::DateTimePatternGenerator* dtpgen = + sharedIntlData.getDateTimePatternGenerator(cx, locale.get()); + if (!dtpgen) { + return false; + } + + result = dn->GetDateTimeField(buffer, field, *dtpgen, fallback); + } + + if (result.isErr()) { + switch (result.unwrapErr()) { + case mozilla::intl::DisplayNamesError::InternalError: + intl::ReportInternalError(cx); + break; + case mozilla::intl::DisplayNamesError::OutOfMemory: + ReportOutOfMemory(cx); + break; + case mozilla::intl::DisplayNamesError::InvalidOption: + ReportInvalidOptionError(cx, type, code); + break; + case mozilla::intl::DisplayNamesError::DuplicateVariantSubtag: + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DUPLICATE_VARIANT_SUBTAG); + break; + case mozilla::intl::DisplayNamesError::InvalidLanguageTag: + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_LANGUAGE_TAG); + break; + } + return false; + } + + JSString* str = buffer.toString(cx); + if (!str) { + return false; + } + + if (str->empty()) { + args.rval().setUndefined(); + } else { + args.rval().setString(str); + } + + return true; +} diff --git a/js/src/builtin/intl/DisplayNames.h b/js/src/builtin/intl/DisplayNames.h new file mode 100644 index 0000000000..9fd6c63a62 --- /dev/null +++ b/js/src/builtin/intl/DisplayNames.h @@ -0,0 +1,79 @@ +/* -*- 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 builtin_intl_DisplayNames_h +#define builtin_intl_DisplayNames_h + +#include <stddef.h> +#include <stdint.h> + +#include "jstypes.h" +#include "NamespaceImports.h" + +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" // JSClass, JSClassOps, js::ClassSpec +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/NativeObject.h" + +struct JS_PUBLIC_API JSContext; + +namespace mozilla::intl { +class DisplayNames; +} + +namespace js { +struct ClassSpec; + +class DisplayNamesObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t LOCALE_DISPLAY_NAMES_SLOT = 1; + static constexpr uint32_t SLOT_COUNT = 3; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals " + "object slot"); + + // Estimated memory use for ULocaleDisplayNames (see IcuMemoryUsage). + static constexpr size_t EstimatedMemoryUse = 1238; + + mozilla::intl::DisplayNames* getDisplayNames() const { + const auto& slot = getFixedSlot(LOCALE_DISPLAY_NAMES_SLOT); + if (slot.isUndefined()) { + return nullptr; + } + return static_cast<mozilla::intl::DisplayNames*>(slot.toPrivate()); + } + + void setDisplayNames(mozilla::intl::DisplayNames* displayNames) { + setFixedSlot(LOCALE_DISPLAY_NAMES_SLOT, PrivateValue(displayNames)); + } + + private: + static const JSClassOps classOps_; + static const ClassSpec classSpec_; + + static void finalize(JS::GCContext* gcx, JSObject* obj); +}; + +/** + * Return the display name for the requested code or undefined if no applicable + * display name was found. + * + * Usage: result = intl_ComputeDisplayName(displayNames, locale, calendar, + * style, languageDisplay, fallback, + * type, code) + */ +[[nodiscard]] extern bool intl_ComputeDisplayName(JSContext* cx, unsigned argc, + Value* vp); + +} // namespace js + +#endif /* builtin_intl_DisplayNames_h */ diff --git a/js/src/builtin/intl/DisplayNames.js b/js/src/builtin/intl/DisplayNames.js new file mode 100644 index 0000000000..00ba2301aa --- /dev/null +++ b/js/src/builtin/intl/DisplayNames.js @@ -0,0 +1,418 @@ +/* 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/. */ + +/** + * Intl.DisplayNames internal properties. + */ +function displayNamesLocaleData() { + // Intl.DisplayNames doesn't support any extension keys. + return {}; +} +var displayNamesInternalProperties = { + localeData: displayNamesLocaleData, + relevantExtensionKeys: [], +}; + +function mozDisplayNamesLocaleData() { + return { + ca: intl_availableCalendars, + default: { + ca: intl_defaultCalendar, + }, + }; +} +var mozDisplayNamesInternalProperties = { + localeData: mozDisplayNamesLocaleData, + relevantExtensionKeys: ["ca"], +}; + +/** + * Intl.DisplayNames ( [ locales [ , options ] ] ) + * + * Compute an internal properties object from |lazyDisplayNamesData|. + */ +function resolveDisplayNamesInternals(lazyDisplayNamesData) { + assert(IsObject(lazyDisplayNamesData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + var mozExtensions = lazyDisplayNamesData.mozExtensions; + + var DisplayNames = mozExtensions + ? mozDisplayNamesInternalProperties + : displayNamesInternalProperties; + + // Compute effective locale. + + // Step 7. + var localeData = DisplayNames.localeData; + + // Step 10. + var r = ResolveLocale( + "DisplayNames", + lazyDisplayNamesData.requestedLocales, + lazyDisplayNamesData.opt, + DisplayNames.relevantExtensionKeys, + localeData + ); + + // Step 12. + internalProps.style = lazyDisplayNamesData.style; + + // Step 14. + var type = lazyDisplayNamesData.type; + internalProps.type = type; + + // Step 16. + internalProps.fallback = lazyDisplayNamesData.fallback; + + // Step 17. + internalProps.locale = r.locale; + + // Step 25. + if (type === "language") { + internalProps.languageDisplay = lazyDisplayNamesData.languageDisplay; + } + + if (mozExtensions) { + internalProps.calendar = r.ca; + } + + // The caller is responsible for associating |internalProps| with the right + // object using |setInternalProperties|. + return internalProps; +} + +/** + * Returns an object containing the DisplayNames internal properties of |obj|. + */ +function getDisplayNamesInternals(obj) { + assert(IsObject(obj), "getDisplayNamesInternals called with non-object"); + assert( + intl_GuardToDisplayNames(obj) !== null, + "getDisplayNamesInternals called with non-DisplayNames" + ); + + var internals = getIntlObjectInternals(obj); + assert( + internals.type === "DisplayNames", + "bad type escaped getIntlObjectInternals" + ); + + // If internal properties have already been computed, use them. + var internalProps = maybeInternalProperties(internals); + if (internalProps) { + return internalProps; + } + + // Otherwise it's time to fully create them. + internalProps = resolveDisplayNamesInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * Intl.DisplayNames ( [ locales [ , options ] ] ) + * + * Initializes an object as a DisplayNames. + * + * This method is complicated a moderate bit by its implementing initialization + * as a *lazy* concept. Everything that must happen now, does -- but we defer + * all the work we can until the object is actually used as a DisplayNames. + * This later work occurs in |resolveDisplayNamesInternals|; steps not noted + * here occur there. + */ +function InitializeDisplayNames(displayNames, locales, options, mozExtensions) { + assert( + IsObject(displayNames), + "InitializeDisplayNames called with non-object" + ); + assert( + intl_GuardToDisplayNames(displayNames) !== null, + "InitializeDisplayNames called with non-DisplayNames" + ); + + // Lazy DisplayNames data has the following structure: + // + // { + // requestedLocales: List of locales, + // + // opt: // opt object computed in InitializeDisplayNames + // { + // localeMatcher: "lookup" / "best fit", + // + // ca: string matching a Unicode extension type, // optional + // } + // + // localeMatcher: "lookup" / "best fit", + // + // style: "narrow" / "short" / "abbreviated" / "long", + // + // type: "language" / "region" / "script" / "currency" / "weekday" / + // "month" / "quarter" / "dayPeriod" / "dateTimeField" + // + // fallback: "code" / "none", + // + // // field present only if type === "language": + // languageDisplay: "dialect" / "standard", + // + // mozExtensions: true / false, + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every DisplayNames lazy data object has *all* these properties, never a + // subset of them. + var lazyDisplayNamesData = std_Object_create(null); + + // Step 3. + var requestedLocales = CanonicalizeLocaleList(locales); + lazyDisplayNamesData.requestedLocales = requestedLocales; + + // Step 4. + if (!IsObject(options)) { + ThrowTypeError( + JSMSG_OBJECT_REQUIRED, + options === null ? "null" : typeof options + ); + } + + // Step 5. + var opt = new_Record(); + lazyDisplayNamesData.opt = opt; + lazyDisplayNamesData.mozExtensions = mozExtensions; + + // Steps 7-8. + var matcher = GetOption( + options, + "localeMatcher", + "string", + ["lookup", "best fit"], + "best fit" + ); + opt.localeMatcher = matcher; + + if (mozExtensions) { + var calendar = GetOption( + options, + "calendar", + "string", + undefined, + undefined + ); + + if (calendar !== undefined) { + calendar = intl_ValidateAndCanonicalizeUnicodeExtensionType( + calendar, + "calendar", + "ca" + ); + } + + opt.ca = calendar; + } + + // Step 10. + var style; + if (mozExtensions) { + style = GetOption( + options, + "style", + "string", + ["narrow", "short", "abbreviated", "long"], + "long" + ); + } else { + style = GetOption( + options, + "style", + "string", + ["narrow", "short", "long"], + "long" + ); + } + + // Step 11. + lazyDisplayNamesData.style = style; + + // Step 12. + var type; + if (mozExtensions) { + type = GetOption( + options, + "type", + "string", + [ + "language", + "region", + "script", + "currency", + "calendar", + "dateTimeField", + "weekday", + "month", + "quarter", + "dayPeriod", + ], + undefined + ); + } else { + type = GetOption( + options, + "type", + "string", + ["language", "region", "script", "currency", "calendar", "dateTimeField"], + undefined + ); + } + + // Step 13. + if (type === undefined) { + ThrowTypeError(JSMSG_UNDEFINED_TYPE); + } + + // Step 14. + lazyDisplayNamesData.type = type; + + // Step 15. + var fallback = GetOption( + options, + "fallback", + "string", + ["code", "none"], + "code" + ); + + // Step 16. + lazyDisplayNamesData.fallback = fallback; + + // Step 24. + var languageDisplay = GetOption( + options, + "languageDisplay", + "string", + ["dialect", "standard"], + "dialect" + ); + + // Step 25. + if (type === "language") { + lazyDisplayNamesData.languageDisplay = languageDisplay; + } + + // We've done everything that must be done now: mark the lazy data as fully + // computed and install it. + initializeIntlObject(displayNames, "DisplayNames", lazyDisplayNamesData); +} + +/** + * Returns the subset of the given locale list for which this locale list has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + */ +function Intl_DisplayNames_supportedLocalesOf(locales /*, options*/) { + var options = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 1. + var availableLocales = "DisplayNames"; + + // Step 2. + var requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. + return SupportedLocales(availableLocales, requestedLocales, options); +} + +/** + * Returns the resolved options for a DisplayNames object. + */ +function Intl_DisplayNames_of(code) { + // Step 1. + var displayNames = this; + + // Steps 2-3. + if ( + !IsObject(displayNames) || + (displayNames = intl_GuardToDisplayNames(displayNames)) === null + ) { + return callFunction( + intl_CallDisplayNamesMethodIfWrapped, + this, + "Intl_DisplayNames_of" + ); + } + + code = ToString(code); + + var internals = getDisplayNamesInternals(displayNames); + + // Unpack the internals object to avoid a slow runtime to selfhosted JS call + // in |intl_ComputeDisplayName()|. + var { + locale, + calendar = "", + style, + type, + languageDisplay = "", + fallback, + } = internals; + + // Steps 5-10. + return intl_ComputeDisplayName( + displayNames, + locale, + calendar, + style, + languageDisplay, + fallback, + type, + code + ); +} + +/** + * Returns the resolved options for a DisplayNames object. + */ +function Intl_DisplayNames_resolvedOptions() { + // Step 1. + var displayNames = this; + + // Steps 2-3. + if ( + !IsObject(displayNames) || + (displayNames = intl_GuardToDisplayNames(displayNames)) === null + ) { + return callFunction( + intl_CallDisplayNamesMethodIfWrapped, + this, + "Intl_DisplayNames_resolvedOptions" + ); + } + + var internals = getDisplayNamesInternals(displayNames); + + // Steps 4-5. + var options = { + locale: internals.locale, + style: internals.style, + type: internals.type, + fallback: internals.fallback, + }; + + // languageDisplay is only present for language display names. + assert( + hasOwn("languageDisplay", internals) === (internals.type === "language"), + "languageDisplay is present iff type is 'language'" + ); + + if (hasOwn("languageDisplay", internals)) { + DefineDataProperty(options, "languageDisplay", internals.languageDisplay); + } + + if (hasOwn("calendar", internals)) { + DefineDataProperty(options, "calendar", internals.calendar); + } + + // Step 6. + return options; +} diff --git a/js/src/builtin/intl/FormatBuffer.h b/js/src/builtin/intl/FormatBuffer.h new file mode 100644 index 0000000000..73d450a546 --- /dev/null +++ b/js/src/builtin/intl/FormatBuffer.h @@ -0,0 +1,155 @@ +/* -*- 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 builtin_intl_FormatBuffer_h +#define builtin_intl_FormatBuffer_h + +#include "mozilla/Assertions.h" +#include "mozilla/Span.h" +#include "mozilla/TextUtils.h" + +#include <stddef.h> +#include <stdint.h> + +#include "gc/Allocator.h" +#include "js/AllocPolicy.h" +#include "js/CharacterEncoding.h" +#include "js/TypeDecls.h" +#include "js/UniquePtr.h" +#include "js/Vector.h" +#include "vm/StringType.h" + +namespace js::intl { + +/** + * A buffer for formatting unified intl data. + */ +template <typename CharT, size_t MinInlineCapacity = 0, + class AllocPolicy = TempAllocPolicy> +class FormatBuffer { + public: + using CharType = CharT; + + // Allow move constructors, but not copy constructors, as this class owns a + // js::Vector. + FormatBuffer(FormatBuffer&& other) noexcept = default; + FormatBuffer& operator=(FormatBuffer&& other) noexcept = default; + + explicit FormatBuffer(AllocPolicy aP = AllocPolicy()) + : buffer_(std::move(aP)) { + // The initial capacity matches the requested minimum inline capacity, as + // long as it doesn't exceed |Vector::kMaxInlineBytes / sizeof(CharT)|. If + // this assertion should ever fail, either reduce |MinInlineCapacity| or + // make the FormatBuffer initialization fallible. + MOZ_ASSERT(buffer_.capacity() == MinInlineCapacity); + if constexpr (MinInlineCapacity > 0) { + // Ensure the full capacity is marked as reserved. + // + // Reserving the minimum inline capacity can never fail, even when + // simulating OOM. + MOZ_ALWAYS_TRUE(buffer_.reserve(MinInlineCapacity)); + } + } + + // Implicitly convert to a Span. + operator mozilla::Span<CharType>() { return buffer_; } + operator mozilla::Span<const CharType>() const { return buffer_; } + + /** + * Ensures the buffer has enough space to accommodate |size| elements. + */ + [[nodiscard]] bool reserve(size_t size) { + // Call |reserve| a second time to ensure its full capacity is marked as + // reserved. + return buffer_.reserve(size) && buffer_.reserve(buffer_.capacity()); + } + + /** + * Returns the raw data inside the buffer. + */ + CharType* data() { return buffer_.begin(); } + + /** + * Returns the count of elements written into the buffer. + */ + size_t length() const { return buffer_.length(); } + + /** + * Returns the buffer's overall capacity. + */ + size_t capacity() const { return buffer_.capacity(); } + + /** + * Resizes the buffer to the given amount of written elements. + */ + void written(size_t amount) { + MOZ_ASSERT(amount <= buffer_.capacity()); + // This sets |buffer_|'s internal size so that it matches how much was + // written. This is necessary because the write happens across FFI + // boundaries. + size_t curLength = length(); + if (amount > curLength) { + buffer_.infallibleGrowByUninitialized(amount - curLength); + } else { + buffer_.shrinkBy(curLength - amount); + } + } + + /** + * Copies the buffer's data to a JSString. + * + * TODO(#1715842) - This should be more explicit on needing to handle OOM + * errors. In this case it returns a nullptr that must be checked, but it may + * not be obvious. + */ + JSLinearString* toString(JSContext* cx) const { + if constexpr (std::is_same_v<CharT, uint8_t> || + std::is_same_v<CharT, unsigned char> || + std::is_same_v<CharT, char>) { + // Handle the UTF-8 encoding case. + return NewStringCopyUTF8N( + cx, JS::UTF8Chars(buffer_.begin(), buffer_.length())); + } else { + // Handle the UTF-16 encoding case. + static_assert(std::is_same_v<CharT, char16_t>); + return NewStringCopyN<CanGC>(cx, buffer_.begin(), buffer_.length()); + } + } + + /** + * Copies the buffer's data to a JSString. The buffer must contain only + * ASCII characters. + */ + JSLinearString* toAsciiString(JSContext* cx) const { + static_assert(std::is_same_v<CharT, char>); + + MOZ_ASSERT(mozilla::IsAscii(buffer_)); + return NewStringCopyN<CanGC>(cx, buffer_.begin(), buffer_.length()); + } + + /** + * Extract this buffer's content as a null-terminated string. + */ + UniquePtr<CharType[], JS::FreePolicy> extractStringZ() { + // Adding the NUL character on an already null-terminated string is likely + // an error. If there's ever a valid use case which triggers this assertion, + // we should change the below code to only conditionally add '\0'. + MOZ_ASSERT_IF(!buffer_.empty(), buffer_.end()[-1] != '\0'); + + if (!buffer_.append('\0')) { + return nullptr; + } + return UniquePtr<CharType[], JS::FreePolicy>( + buffer_.extractOrCopyRawBuffer()); + } + + private: + js::Vector<CharT, MinInlineCapacity, AllocPolicy> buffer_; +}; + +} // namespace js::intl + +#endif /* builtin_intl_FormatBuffer_h */ diff --git a/js/src/builtin/intl/IcuMemoryUsage.java b/js/src/builtin/intl/IcuMemoryUsage.java new file mode 100644 index 0000000000..15e4f1fd38 --- /dev/null +++ b/js/src/builtin/intl/IcuMemoryUsage.java @@ -0,0 +1,260 @@ +/* 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/. */ + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.regex.*; +import java.util.stream.Collectors; + +/** + * Java program to estimate the memory usage of ICU objects (bug 1585536). + * + * It computes for each Intl constructor the amount of allocated memory. We're + * currently using the maximum memory ("max" in the output) to estimate the + * memory consumption of ICU objects. + * + * Insert before {@code JS_InitWithFailureDiagnostic} in "js.cpp": + * + * <pre> + * <code> + * JS_SetICUMemoryFunctions( + * [](const void*, size_t size) { + * void* ptr = malloc(size); + * if (ptr) { + * printf(" alloc: %p -> %zu\n", ptr, size); + * } + * return ptr; + * }, + * [](const void*, void* p, size_t size) { + * void* ptr = realloc(p, size); + * if (p) { + * printf(" realloc: %p -> %p -> %zu\n", p, ptr, size); + * } else { + * printf(" alloc: %p -> %zu\n", ptr, size); + * } + * return ptr; + * }, + * [](const void*, void* p) { + * if (p) { + * printf(" free: %p\n", p); + * } + * free(p); + * }); + * </code> + * </pre> + * + * Run this script with: + * {@code java --enable-preview --source=14 IcuMemoryUsage.java $MOZ_JS_SHELL}. + */ +@SuppressWarnings("preview") +public class IcuMemoryUsage { + private enum Phase { + None, Create, Init, Destroy, Collect, Quit + } + + private static final class Memory { + private Phase phase = Phase.None; + private HashMap<Long, Map.Entry<Phase, Long>> allocations = new HashMap<>(); + private HashSet<Long> freed = new HashSet<>(); + private HashMap<Long, Map.Entry<Phase, Long>> completeAllocations = new HashMap<>(); + private int allocCount = 0; + private ArrayList<Long> allocSizes = new ArrayList<>(); + + void transition(Phase nextPhase) { + assert phase.ordinal() + 1 == nextPhase.ordinal() || (phase == Phase.Collect && nextPhase == Phase.Create); + phase = nextPhase; + + // Create a clean slate when starting a new create cycle or before termination. + if (phase == Phase.Create || phase == Phase.Quit) { + transferAllocations(); + } + + // Only measure the allocation size when creating the second object with the + // same locale. + if (phase == Phase.Collect && ++allocCount % 2 == 0) { + long size = allocations.values().stream().map(Map.Entry::getValue).reduce(0L, (a, c) -> a + c); + allocSizes.add(size); + } + } + + void transferAllocations() { + completeAllocations.putAll(allocations); + completeAllocations.keySet().removeAll(freed); + allocations.clear(); + freed.clear(); + } + + void alloc(long ptr, long size) { + allocations.put(ptr, Map.entry(phase, size)); + } + + void realloc(long oldPtr, long newPtr, long size) { + free(oldPtr); + allocations.put(newPtr, Map.entry(phase, size)); + } + + void free(long ptr) { + if (allocations.remove(ptr) == null) { + freed.add(ptr); + } + } + + LongSummaryStatistics statistics() { + return allocSizes.stream().collect(Collectors.summarizingLong(Long::valueOf)); + } + + double percentile(double p) { + var size = allocSizes.size(); + return allocSizes.stream().sorted().skip((long) ((size - 1) * p)).limit(2 - size % 2) + .mapToDouble(Long::doubleValue).average().getAsDouble(); + } + + long persistent() { + return completeAllocations.values().stream().map(Map.Entry::getValue).reduce(0L, (a, c) -> a + c); + } + } + + private static long parseSize(Matcher m, int group) { + return Long.parseLong(m.group(group), 10); + } + + private static long parsePointer(Matcher m, int group) { + return Long.parseLong(m.group(group), 16); + } + + private static void measure(String exec, String constructor, String description, String initializer) throws IOException { + var locales = Arrays.stream(Locale.getAvailableLocales()).map(Locale::toLanguageTag).sorted() + .collect(Collectors.toUnmodifiableList()); + + var pb = new ProcessBuilder(exec, "--file=-", "--", constructor, initializer, + locales.stream().collect(Collectors.joining(","))); + var process = pb.start(); + + try (var writer = new BufferedWriter( + new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) { + writer.write(sourceCode); + writer.flush(); + } + + var memory = new Memory(); + + try (var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + var reAlloc = Pattern.compile("\\s+alloc: 0x(\\p{XDigit}+) -> (\\p{Digit}+)"); + var reRealloc = Pattern.compile("\\s+realloc: 0x(\\p{XDigit}+) -> 0x(\\p{XDigit}+) -> (\\p{Digit}+)"); + var reFree = Pattern.compile("\\s+free: 0x(\\p{XDigit}+)"); + + String line; + while ((line = reader.readLine()) != null) { + Matcher m; + if ((m = reAlloc.matcher(line)).matches()) { + var ptr = parsePointer(m, 1); + var size = parseSize(m, 2); + memory.alloc(ptr, size); + } else if ((m = reRealloc.matcher(line)).matches()) { + var oldPtr = parsePointer(m, 1); + var newPtr = parsePointer(m, 2); + var size = parseSize(m, 3); + memory.realloc(oldPtr, newPtr, size); + } else if ((m = reFree.matcher(line)).matches()) { + var ptr = parsePointer(m, 1); + memory.free(ptr); + } else { + memory.transition(Phase.valueOf(line)); + } + } + } + + try (var errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String line; + while ((line = errorReader.readLine()) != null) { + System.err.println(line); + } + } + + var stats = memory.statistics(); + + System.out.printf("%s%n", description); + System.out.printf(" max: %d%n", stats.getMax()); + System.out.printf(" min: %d%n", stats.getMin()); + System.out.printf(" avg: %.0f%n", stats.getAverage()); + System.out.printf(" 50p: %.0f%n", memory.percentile(0.50)); + System.out.printf(" 75p: %.0f%n", memory.percentile(0.75)); + System.out.printf(" 85p: %.0f%n", memory.percentile(0.85)); + System.out.printf(" 95p: %.0f%n", memory.percentile(0.95)); + System.out.printf(" 99p: %.0f%n", memory.percentile(0.99)); + System.out.printf(" mem: %d%n", memory.persistent()); + + memory.transferAllocations(); + assert memory.persistent() == 0 : String.format("Leaked %d bytes", memory.persistent()); + } + + public static void main(String[] args) throws IOException { + if (args.length == 0) { + throw new RuntimeException("The first argument must point to the SpiderMonkey shell executable"); + } + + record Entry (String constructor, String description, String initializer) { + public static Entry of(String constructor, String description, String initializer) { + return new Entry(constructor, description, initializer); + } + + public static Entry of(String constructor, String initializer) { + return new Entry(constructor, constructor, initializer); + } + } + + var objects = new ArrayList<Entry>(); + objects.add(Entry.of("Collator", "o.compare('a', 'b')")); + objects.add(Entry.of("DateTimeFormat", "DateTimeFormat (UDateFormat)", "o.format(0)")); + objects.add(Entry.of("DateTimeFormat", "DateTimeFormat (UDateFormat+UDateIntervalFormat)", + "o.formatRange(0, 24*60*60*1000)")); + objects.add(Entry.of("DisplayNames", "o.of('en')")); + objects.add(Entry.of("ListFormat", "o.format(['a', 'b'])")); + objects.add(Entry.of("NumberFormat", "o.format(0)")); + objects.add(Entry.of("NumberFormat", "NumberFormat (UNumberRangeFormatter)", + "o.formatRange(0, 1000)")); + objects.add(Entry.of("PluralRules", "o.select(0)")); + objects.add(Entry.of("RelativeTimeFormat", "o.format(0, 'hour')")); + + for (var entry : objects) { + measure(args[0], entry.constructor, entry.description, entry.initializer); + } + } + + private static final String sourceCode = """ +const constructorName = scriptArgs[0]; +const initializer = Function("o", scriptArgs[1]); +const locales = scriptArgs[2].split(","); + +const extras = {}; +addIntlExtras(extras); + +for (let i = 0; i < locales.length; ++i) { + // Loop twice in case the first time we create an object with a new locale + // allocates additional memory when loading the locale data. + for (let j = 0; j < 2; ++j) { + let constructor = Intl[constructorName]; + let options = undefined; + if (constructor === Intl.DisplayNames) { + options = {type: "language"}; + } + + print("Create"); + let obj = new constructor(locales[i], options); + + print("Init"); + initializer(obj); + + print("Destroy"); + gc(); + gc(); + print("Collect"); + } +} + +print("Quit"); +quit(); +"""; +} diff --git a/js/src/builtin/intl/IntlObject.cpp b/js/src/builtin/intl/IntlObject.cpp new file mode 100644 index 0000000000..299964d07b --- /dev/null +++ b/js/src/builtin/intl/IntlObject.cpp @@ -0,0 +1,910 @@ +/* -*- 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/. */ + +/* Implementation of the Intl object and its non-constructor properties. */ + +#include "builtin/intl/IntlObject.h" + +#include "mozilla/Assertions.h" +#include "mozilla/intl/Calendar.h" +#include "mozilla/intl/Collator.h" +#include "mozilla/intl/Currency.h" +#include "mozilla/intl/Locale.h" +#include "mozilla/intl/MeasureUnitGenerated.h" +#include "mozilla/intl/TimeZone.h" + +#include <algorithm> +#include <array> +#include <cstring> +#include <iterator> +#include <string_view> + +#include "builtin/Array.h" +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/FormatBuffer.h" +#include "builtin/intl/NumberingSystemsGenerated.h" +#include "builtin/intl/SharedIntlData.h" +#include "builtin/intl/StringAsciiChars.h" +#include "ds/Sort.h" +#include "js/Class.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/GCAPI.h" +#include "js/GCVector.h" +#include "js/PropertySpec.h" +#include "js/Result.h" +#include "js/StableStringChars.h" +#include "vm/GlobalObject.h" +#include "vm/JSAtom.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/StringType.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +/******************** Intl ********************/ + +bool js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + UniqueChars locale = intl::EncodeLocale(cx, args[0].toString()); + if (!locale) { + return false; + } + + auto result = mozilla::intl::Calendar::TryCreate(locale.get()); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + auto calendar = result.unwrap(); + + RootedObject info(cx, NewPlainObject(cx)); + if (!info) { + return false; + } + + RootedValue v(cx); + + v.setInt32(static_cast<int32_t>(calendar->GetFirstDayOfWeek())); + if (!DefineDataProperty(cx, info, cx->names().firstDayOfWeek, v)) { + return false; + } + + v.setInt32(calendar->GetMinimalDaysInFirstWeek()); + if (!DefineDataProperty(cx, info, cx->names().minDays, v)) { + return false; + } + + Rooted<ArrayObject*> weekendArray(cx, NewDenseEmptyArray(cx)); + if (!weekendArray) { + return false; + } + + auto weekend = calendar->GetWeekend(); + if (weekend.isErr()) { + intl::ReportInternalError(cx, weekend.unwrapErr()); + return false; + } + + for (auto day : weekend.unwrap()) { + if (!NewbornArrayPush(cx, weekendArray, + Int32Value(static_cast<int32_t>(day)))) { + return false; + } + } + + v.setObject(*weekendArray); + if (!DefineDataProperty(cx, info, cx->names().weekend, v)) { + return false; + } + + args.rval().setObject(*info); + return true; +} + +static void ReportBadKey(JSContext* cx, JSString* key) { + if (UniqueChars chars = QuoteString(cx, key, '"')) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, + chars.get()); + } +} + +static bool SameOrParentLocale(JSLinearString* locale, + JSLinearString* otherLocale) { + // Return true if |locale| is the same locale as |otherLocale|. + if (locale->length() == otherLocale->length()) { + return EqualStrings(locale, otherLocale); + } + + // Also return true if |locale| is the parent locale of |otherLocale|. + if (locale->length() < otherLocale->length()) { + return HasSubstringAt(otherLocale, locale, 0) && + otherLocale->latin1OrTwoByteChar(locale->length()) == '-'; + } + + return false; +} + +using SupportedLocaleKind = js::intl::SharedIntlData::SupportedLocaleKind; + +// 9.2.2 BestAvailableLocale ( availableLocales, locale ) +static JS::Result<JSLinearString*> BestAvailableLocale( + JSContext* cx, SupportedLocaleKind kind, Handle<JSLinearString*> locale, + Handle<JSLinearString*> defaultLocale) { + // In the spec, [[availableLocales]] is formally a list of all available + // locales. But in our implementation, it's an *incomplete* list, not + // necessarily including the default locale (and all locales implied by it, + // e.g. "de" implied by "de-CH"), if that locale isn't in every + // [[availableLocales]] list (because that locale is supported through + // fallback, e.g. "de-CH" supported through "de"). + // + // If we're considering the default locale, augment the spec loop with + // additional checks to also test whether the current prefix is a prefix of + // the default locale. + + intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + + auto findLast = [](const auto* chars, size_t length) { + auto rbegin = std::make_reverse_iterator(chars + length); + auto rend = std::make_reverse_iterator(chars); + auto p = std::find(rbegin, rend, '-'); + + // |dist(chars, p.base())| is equal to |dist(p, rend)|, pick whichever you + // find easier to reason about when using reserve iterators. + ptrdiff_t r = std::distance(chars, p.base()); + MOZ_ASSERT(r == std::distance(p, rend)); + + // But always subtract one to convert from the reverse iterator result to + // the correspoding forward iterator value, because reserve iterators point + // to one element past the forward iterator value. + return r - 1; + }; + + // Step 1. + Rooted<JSLinearString*> candidate(cx, locale); + + // Step 2. + while (true) { + // Step 2.a. + bool supported = false; + if (!sharedIntlData.isSupportedLocale(cx, kind, candidate, &supported)) { + return cx->alreadyReportedError(); + } + if (supported) { + return candidate.get(); + } + + if (defaultLocale && SameOrParentLocale(candidate, defaultLocale)) { + return candidate.get(); + } + + // Step 2.b. + ptrdiff_t pos; + if (candidate->hasLatin1Chars()) { + JS::AutoCheckCannotGC nogc; + pos = findLast(candidate->latin1Chars(nogc), candidate->length()); + } else { + JS::AutoCheckCannotGC nogc; + pos = findLast(candidate->twoByteChars(nogc), candidate->length()); + } + + if (pos < 0) { + return nullptr; + } + + // Step 2.c. + size_t length = size_t(pos); + if (length >= 2 && candidate->latin1OrTwoByteChar(length - 2) == '-') { + length -= 2; + } + + // Step 2.d. + candidate = NewDependentString(cx, candidate, 0, length); + if (!candidate) { + return cx->alreadyReportedError(); + } + } +} + +// 9.2.2 BestAvailableLocale ( availableLocales, locale ) +// +// Carries an additional third argument in our implementation to provide the +// default locale. See the doc-comment in the header file. +bool js::intl_BestAvailableLocale(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + + SupportedLocaleKind kind; + { + JSLinearString* typeStr = args[0].toString()->ensureLinear(cx); + if (!typeStr) { + return false; + } + + if (StringEqualsLiteral(typeStr, "Collator")) { + kind = SupportedLocaleKind::Collator; + } else if (StringEqualsLiteral(typeStr, "DateTimeFormat")) { + kind = SupportedLocaleKind::DateTimeFormat; + } else if (StringEqualsLiteral(typeStr, "DisplayNames")) { + kind = SupportedLocaleKind::DisplayNames; + } else if (StringEqualsLiteral(typeStr, "ListFormat")) { + kind = SupportedLocaleKind::ListFormat; + } else if (StringEqualsLiteral(typeStr, "NumberFormat")) { + kind = SupportedLocaleKind::NumberFormat; + } else if (StringEqualsLiteral(typeStr, "PluralRules")) { + kind = SupportedLocaleKind::PluralRules; + } else { + MOZ_ASSERT(StringEqualsLiteral(typeStr, "RelativeTimeFormat")); + kind = SupportedLocaleKind::RelativeTimeFormat; + } + } + + Rooted<JSLinearString*> locale(cx, args[1].toString()->ensureLinear(cx)); + if (!locale) { + return false; + } + +#ifdef DEBUG + { + MOZ_ASSERT(StringIsAscii(locale), "language tags are ASCII-only"); + + // |locale| is a structurally valid language tag. + mozilla::intl::Locale tag; + + using ParserError = mozilla::intl::LocaleParser::ParserError; + mozilla::Result<mozilla::Ok, ParserError> parse_result = Ok(); + { + intl::StringAsciiChars chars(locale); + if (!chars.init(cx)) { + return false; + } + + parse_result = mozilla::intl::LocaleParser::TryParse(chars, tag); + } + + if (parse_result.isErr()) { + MOZ_ASSERT(parse_result.unwrapErr() == ParserError::OutOfMemory, + "locale is a structurally valid language tag"); + + intl::ReportInternalError(cx); + return false; + } + + MOZ_ASSERT(!tag.GetUnicodeExtension(), + "locale must contain no Unicode extensions"); + + if (auto result = tag.Canonicalize(); result.isErr()) { + MOZ_ASSERT( + result.unwrapErr() != + mozilla::intl::Locale::CanonicalizationError::DuplicateVariant); + intl::ReportInternalError(cx); + return false; + } + + intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + if (auto result = tag.ToString(buffer); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + JSLinearString* tagStr = buffer.toString(cx); + if (!tagStr) { + return false; + } + + MOZ_ASSERT(EqualStrings(locale, tagStr), + "locale is a canonicalized language tag"); + } +#endif + + MOZ_ASSERT(args[2].isNull() || args[2].isString()); + + Rooted<JSLinearString*> defaultLocale(cx); + if (args[2].isString()) { + defaultLocale = args[2].toString()->ensureLinear(cx); + if (!defaultLocale) { + return false; + } + } + + JSString* result; + JS_TRY_VAR_OR_RETURN_FALSE( + cx, result, BestAvailableLocale(cx, kind, locale, defaultLocale)); + + if (result) { + args.rval().setString(result); + } else { + args.rval().setUndefined(); + } + return true; +} + +bool js::intl_supportedLocaleOrFallback(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + Rooted<JSLinearString*> locale(cx, args[0].toString()->ensureLinear(cx)); + if (!locale) { + return false; + } + + mozilla::intl::Locale tag; + bool canParseLocale = false; + if (StringIsAscii(locale)) { + intl::StringAsciiChars chars(locale); + if (!chars.init(cx)) { + return false; + } + + // Tell the analysis the |tag.canonicalize()| method can't GC. + JS::AutoSuppressGCAnalysis nogc; + + canParseLocale = mozilla::intl::LocaleParser::TryParse(chars, tag).isOk() && + tag.Canonicalize().isOk(); + } + + Rooted<JSLinearString*> candidate(cx); + if (!canParseLocale) { + candidate = NewStringCopyZ<CanGC>(cx, intl::LastDitchLocale()); + if (!candidate) { + return false; + } + } else { + // The default locale must be in [[AvailableLocales]], and that list must + // not contain any locales with Unicode extension sequences, so remove any + // present in the candidate. + tag.ClearUnicodeExtension(); + + intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + if (auto result = tag.ToString(buffer); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + candidate = buffer.toAsciiString(cx); + if (!candidate) { + return false; + } + + // Certain old-style language tags lack a script code, but in current + // usage they *would* include a script code. Map these over to modern + // forms. + for (const auto& mapping : js::intl::oldStyleLanguageTagMappings) { + const char* oldStyle = mapping.oldStyle; + const char* modernStyle = mapping.modernStyle; + + if (StringEqualsAscii(candidate, oldStyle)) { + candidate = NewStringCopyZ<CanGC>(cx, modernStyle); + if (!candidate) { + return false; + } + break; + } + } + } + + // 9.1 Internal slots of Service Constructors + // + // - [[AvailableLocales]] is a List [...]. The list must include the value + // returned by the DefaultLocale abstract operation (6.2.4), [...]. + // + // That implies we must ignore any candidate which isn't supported by all + // Intl service constructors. + + Rooted<JSLinearString*> supportedCollator(cx); + JS_TRY_VAR_OR_RETURN_FALSE( + cx, supportedCollator, + BestAvailableLocale(cx, SupportedLocaleKind::Collator, candidate, + nullptr)); + + Rooted<JSLinearString*> supportedDateTimeFormat(cx); + JS_TRY_VAR_OR_RETURN_FALSE( + cx, supportedDateTimeFormat, + BestAvailableLocale(cx, SupportedLocaleKind::DateTimeFormat, candidate, + nullptr)); + +#ifdef DEBUG + // Note: We don't test the supported locales of the remaining Intl service + // constructors, because the set of supported locales is exactly equal to + // the set of supported locales of Intl.DateTimeFormat. + for (auto kind : + {SupportedLocaleKind::DisplayNames, SupportedLocaleKind::ListFormat, + SupportedLocaleKind::NumberFormat, SupportedLocaleKind::PluralRules, + SupportedLocaleKind::RelativeTimeFormat}) { + JSLinearString* supported; + JS_TRY_VAR_OR_RETURN_FALSE( + cx, supported, BestAvailableLocale(cx, kind, candidate, nullptr)); + + MOZ_ASSERT(!!supported == !!supportedDateTimeFormat); + MOZ_ASSERT_IF(supported, EqualStrings(supported, supportedDateTimeFormat)); + } +#endif + + // Accept the candidate locale if it is supported by all Intl service + // constructors. + if (supportedCollator && supportedDateTimeFormat) { + // Use the actually supported locale instead of the candidate locale. For + // example when the candidate locale "en-US-posix" is supported through + // "en-US", use "en-US" as the default locale. + // + // Also prefer the supported locale with more subtags. For example when + // requesting "de-CH" and Intl.DateTimeFormat supports "de-CH", but + // Intl.Collator only "de", still return "de-CH" as the result. + if (SameOrParentLocale(supportedCollator, supportedDateTimeFormat)) { + candidate = supportedDateTimeFormat; + } else { + candidate = supportedCollator; + } + } else { + candidate = NewStringCopyZ<CanGC>(cx, intl::LastDitchLocale()); + if (!candidate) { + return false; + } + } + + args.rval().setString(candidate); + return true; +} + +using StringList = GCVector<JSLinearString*>; + +/** + * Create a sorted array from a list of strings. + */ +static ArrayObject* CreateArrayFromList(JSContext* cx, + MutableHandle<StringList> list) { + // Reserve scratch space for MergeSort(). + size_t initialLength = list.length(); + if (!list.growBy(initialLength)) { + return nullptr; + } + + // Sort all strings in alphabetical order. + MOZ_ALWAYS_TRUE( + MergeSort(list.begin(), initialLength, list.begin() + initialLength, + [](const auto* a, const auto* b, bool* lessOrEqual) { + *lessOrEqual = CompareStrings(a, b) <= 0; + return true; + })); + + // Ensure we don't add duplicate entries to the array. + auto* end = std::unique( + list.begin(), list.begin() + initialLength, + [](const auto* a, const auto* b) { return EqualStrings(a, b); }); + + // std::unique leaves the elements after |end| with an unspecified value, so + // remove them first. And also delete the elements in the scratch space. + list.shrinkBy(std::distance(end, list.end())); + + // And finally copy the strings into the result array. + auto* array = NewDenseFullyAllocatedArray(cx, list.length()); + if (!array) { + return nullptr; + } + array->setDenseInitializedLength(list.length()); + + for (size_t i = 0; i < list.length(); ++i) { + array->initDenseElement(i, StringValue(list[i])); + } + + return array; +} + +/** + * Create an array from a sorted list of strings. + */ +template <size_t N> +static ArrayObject* CreateArrayFromSortedList( + JSContext* cx, const std::array<const char*, N>& list) { + // Ensure the list is sorted and doesn't contain duplicates. +#ifdef DEBUG + // See bug 1583449 for why the lambda can't be in the MOZ_ASSERT. + auto isLargerThanOrEqual = [](const auto& a, const auto& b) { + return std::strcmp(a, b) >= 0; + }; +#endif + MOZ_ASSERT(std::adjacent_find(std::begin(list), std::end(list), + isLargerThanOrEqual) == std::end(list)); + + size_t length = std::size(list); + + Rooted<ArrayObject*> array(cx, NewDenseFullyAllocatedArray(cx, length)); + if (!array) { + return nullptr; + } + array->ensureDenseInitializedLength(0, length); + + for (size_t i = 0; i < length; ++i) { + auto* str = NewStringCopyZ<CanGC>(cx, list[i]); + if (!str) { + return nullptr; + } + array->initDenseElement(i, StringValue(str)); + } + return array; +} + +/** + * Create an array from an intl::Enumeration. + */ +template <const auto& unsupported, class Enumeration> +static bool EnumerationIntoList(JSContext* cx, Enumeration values, + MutableHandle<StringList> list) { + for (auto value : values) { + if (value.isErr()) { + intl::ReportInternalError(cx); + return false; + } + auto span = value.unwrap(); + + // Skip over known, unsupported values. + std::string_view sv(span.data(), span.size()); + if (std::any_of(std::begin(unsupported), std::end(unsupported), + [sv](const auto& e) { return sv == e; })) { + continue; + } + + auto* string = NewStringCopy<CanGC>(cx, span); + if (!string) { + return false; + } + if (!list.append(string)) { + return false; + } + } + + return true; +} + +/** + * Returns the list of calendar types which mustn't be returned by + * |Intl.supportedValuesOf()|. + */ +static constexpr auto UnsupportedCalendars() { + // No calendar values are currently unsupported. + return std::array<const char*, 0>{}; +} + +// Defined outside of the function to workaround bugs in GCC<9. +// Also see <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85589>. +static constexpr auto UnsupportedCalendarsArray = UnsupportedCalendars(); + +/** + * AvailableCalendars ( ) + */ +static ArrayObject* AvailableCalendars(JSContext* cx) { + Rooted<StringList> list(cx, StringList(cx)); + + { + // Hazard analysis complains that the mozilla::Result destructor calls a + // GC function, which is unsound when returning an unrooted value. Work + // around this issue by restricting the lifetime of |keywords| to a + // separate block. + auto keywords = mozilla::intl::Calendar::GetBcp47KeywordValuesForLocale(""); + if (keywords.isErr()) { + intl::ReportInternalError(cx, keywords.unwrapErr()); + return nullptr; + } + + static constexpr auto& unsupported = UnsupportedCalendarsArray; + + if (!EnumerationIntoList<unsupported>(cx, keywords.unwrap(), &list)) { + return nullptr; + } + } + + return CreateArrayFromList(cx, &list); +} + +/** + * Returns the list of collation types which mustn't be returned by + * |Intl.supportedValuesOf()|. + */ +static constexpr auto UnsupportedCollations() { + return std::array{ + "search", + "standard", + }; +} + +// Defined outside of the function to workaround bugs in GCC<9. +// Also see <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85589>. +static constexpr auto UnsupportedCollationsArray = UnsupportedCollations(); + +/** + * AvailableCollations ( ) + */ +static ArrayObject* AvailableCollations(JSContext* cx) { + Rooted<StringList> list(cx, StringList(cx)); + + { + // Hazard analysis complains that the mozilla::Result destructor calls a + // GC function, which is unsound when returning an unrooted value. Work + // around this issue by restricting the lifetime of |keywords| to a + // separate block. + auto keywords = mozilla::intl::Collator::GetBcp47KeywordValues(); + if (keywords.isErr()) { + intl::ReportInternalError(cx, keywords.unwrapErr()); + return nullptr; + } + + static constexpr auto& unsupported = UnsupportedCollationsArray; + + if (!EnumerationIntoList<unsupported>(cx, keywords.unwrap(), &list)) { + return nullptr; + } + } + + return CreateArrayFromList(cx, &list); +} + +/** + * Returns a list of known, unsupported currencies which are returned by + * |Currency::GetISOCurrencies()|. + */ +static constexpr auto UnsupportedCurrencies() { + // "MVP" is also marked with "questionable, remove?" in ucurr.cpp, but only + // this single currency code isn't supported by |Intl.DisplayNames| and + // therefore must be excluded by |Intl.supportedValuesOf|. + return std::array{ + "LSM", // https://unicode-org.atlassian.net/browse/ICU-21687 + }; +} + +/** + * Return a list of known, missing currencies which aren't returned by + * |Currency::GetISOCurrencies()|. + */ +static constexpr auto MissingCurrencies() { + return std::array{ + "SLE", // https://unicode-org.atlassian.net/browse/ICU-21989 + "VED", // https://unicode-org.atlassian.net/browse/ICU-21989 + }; +} + +// Defined outside of the function to workaround bugs in GCC<9. +// Also see <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85589>. +static constexpr auto UnsupportedCurrenciesArray = UnsupportedCurrencies(); +static constexpr auto MissingCurrenciesArray = MissingCurrencies(); + +/** + * AvailableCurrencies ( ) + */ +static ArrayObject* AvailableCurrencies(JSContext* cx) { + Rooted<StringList> list(cx, StringList(cx)); + + { + // Hazard analysis complains that the mozilla::Result destructor calls a + // GC function, which is unsound when returning an unrooted value. Work + // around this issue by restricting the lifetime of |currencies| to a + // separate block. + auto currencies = mozilla::intl::Currency::GetISOCurrencies(); + if (currencies.isErr()) { + intl::ReportInternalError(cx, currencies.unwrapErr()); + return nullptr; + } + + static constexpr auto& unsupported = UnsupportedCurrenciesArray; + + if (!EnumerationIntoList<unsupported>(cx, currencies.unwrap(), &list)) { + return nullptr; + } + } + + // Add known missing values. + for (const char* value : MissingCurrenciesArray) { + auto* string = NewStringCopyZ<CanGC>(cx, value); + if (!string) { + return nullptr; + } + if (!list.append(string)) { + return nullptr; + } + } + + return CreateArrayFromList(cx, &list); +} + +/** + * AvailableNumberingSystems ( ) + */ +static ArrayObject* AvailableNumberingSystems(JSContext* cx) { + static constexpr std::array numberingSystems = { + NUMBERING_SYSTEMS_WITH_SIMPLE_DIGIT_MAPPINGS}; + + return CreateArrayFromSortedList(cx, numberingSystems); +} + +/** + * AvailableTimeZones ( ) + */ +static ArrayObject* AvailableTimeZones(JSContext* cx) { + // Unsorted list of canonical time zone names, possibly containing + // duplicates. + Rooted<StringList> timeZones(cx, StringList(cx)); + + intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + auto iterResult = sharedIntlData.availableTimeZonesIteration(cx); + if (iterResult.isErr()) { + return nullptr; + } + auto iter = iterResult.unwrap(); + + Rooted<JSAtom*> validatedTimeZone(cx); + Rooted<JSAtom*> ianaTimeZone(cx); + for (; !iter.done(); iter.next()) { + validatedTimeZone = iter.get(); + + // Canonicalize the time zone before adding it to the result array. + + // Some time zone names are canonicalized differently by ICU -- handle + // those first. + ianaTimeZone.set(nullptr); + if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA( + cx, validatedTimeZone, &ianaTimeZone)) { + return nullptr; + } + + JSLinearString* timeZone; + if (ianaTimeZone) { + cx->markAtom(ianaTimeZone); + + timeZone = ianaTimeZone; + } else { + // Call into ICU to canonicalize the time zone. + + JS::AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, validatedTimeZone)) { + return nullptr; + } + + intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> + canonicalTimeZone(cx); + auto result = mozilla::intl::TimeZone::GetCanonicalTimeZoneID( + stableChars.twoByteRange(), canonicalTimeZone); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + + timeZone = canonicalTimeZone.toString(cx); + if (!timeZone) { + return nullptr; + } + + // Canonicalize both to "UTC" per CanonicalizeTimeZoneName(). + if (StringEqualsLiteral(timeZone, "Etc/UTC") || + StringEqualsLiteral(timeZone, "Etc/GMT")) { + timeZone = cx->names().UTC; + } + } + + if (!timeZones.append(timeZone)) { + return nullptr; + } + } + + return CreateArrayFromList(cx, &timeZones); +} + +template <size_t N> +constexpr auto MeasurementUnitNames( + const mozilla::intl::SimpleMeasureUnit (&units)[N]) { + std::array<const char*, N> array = {}; + for (size_t i = 0; i < N; ++i) { + array[i] = units[i].name; + } + return array; +} + +/** + * AvailableUnits ( ) + */ +static ArrayObject* AvailableUnits(JSContext* cx) { + static constexpr auto simpleMeasureUnitNames = + MeasurementUnitNames(mozilla::intl::simpleMeasureUnits); + + return CreateArrayFromSortedList(cx, simpleMeasureUnitNames); +} + +bool js::intl_SupportedValuesOf(JSContext* cx, unsigned argc, JS::Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + JSLinearString* key = args[0].toString()->ensureLinear(cx); + if (!key) { + return false; + } + + ArrayObject* list; + if (StringEqualsLiteral(key, "calendar")) { + list = AvailableCalendars(cx); + } else if (StringEqualsLiteral(key, "collation")) { + list = AvailableCollations(cx); + } else if (StringEqualsLiteral(key, "currency")) { + list = AvailableCurrencies(cx); + } else if (StringEqualsLiteral(key, "numberingSystem")) { + list = AvailableNumberingSystems(cx); + } else if (StringEqualsLiteral(key, "timeZone")) { + list = AvailableTimeZones(cx); + } else if (StringEqualsLiteral(key, "unit")) { + list = AvailableUnits(cx); + } else { + ReportBadKey(cx, key); + return false; + } + if (!list) { + return false; + } + + args.rval().setObject(*list); + return true; +} + +static bool intl_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().Intl); + return true; +} + +static const JSFunctionSpec intl_static_methods[] = { + JS_FN(js_toSource_str, intl_toSource, 0, 0), + JS_SELF_HOSTED_FN("getCanonicalLocales", "Intl_getCanonicalLocales", 1, 0), + JS_SELF_HOSTED_FN("supportedValuesOf", "Intl_supportedValuesOf", 1, 0), + JS_FS_END}; + +static const JSPropertySpec intl_static_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Intl", JSPROP_READONLY), JS_PS_END}; + +static JSObject* CreateIntlObject(JSContext* cx, JSProtoKey key) { + RootedObject proto(cx, &cx->global()->getObjectPrototype()); + + // The |Intl| object is just a plain object with some "static" function + // properties and some constructor properties. + return NewTenuredObjectWithGivenProto(cx, &IntlClass, proto); +} + +/** + * Initializes the Intl Object and its standard built-in properties. + * Spec: ECMAScript Internationalization API Specification, 8.0, 8.1 + */ +static bool IntlClassFinish(JSContext* cx, HandleObject intl, + HandleObject proto) { + // Add the constructor properties. + RootedId ctorId(cx); + RootedValue ctorValue(cx); + for (const auto& protoKey : + {JSProto_Collator, JSProto_DateTimeFormat, JSProto_DisplayNames, + JSProto_ListFormat, JSProto_Locale, JSProto_NumberFormat, + JSProto_PluralRules, JSProto_RelativeTimeFormat}) { + JSObject* ctor = GlobalObject::getOrCreateConstructor(cx, protoKey); + if (!ctor) { + return false; + } + + ctorId = NameToId(ClassName(protoKey, cx)); + ctorValue.setObject(*ctor); + if (!DefineDataProperty(cx, intl, ctorId, ctorValue, 0)) { + return false; + } + } + + return true; +} + +static const ClassSpec IntlClassSpec = { + CreateIntlObject, nullptr, intl_static_methods, intl_static_properties, + nullptr, nullptr, IntlClassFinish}; + +const JSClass js::IntlClass = {"Intl", JSCLASS_HAS_CACHED_PROTO(JSProto_Intl), + JS_NULL_CLASS_OPS, &IntlClassSpec}; diff --git a/js/src/builtin/intl/IntlObject.h b/js/src/builtin/intl/IntlObject.h new file mode 100644 index 0000000000..5b79f74e92 --- /dev/null +++ b/js/src/builtin/intl/IntlObject.h @@ -0,0 +1,82 @@ +/* -*- 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 builtin_intl_IntlObject_h +#define builtin_intl_IntlObject_h + +#include "js/TypeDecls.h" + +namespace js { + +extern const JSClass IntlClass; + +/** + * Returns a plain object with calendar information for a single valid locale + * (callers must perform this validation). The object will have these + * properties: + * + * firstDayOfWeek + * an integer in the range 1=Monday to 7=Sunday indicating the day + * considered the first day of the week in calendars, e.g. 7 for en-US, + * 1 for en-GB, 7 for bn-IN + * minDays + * an integer in the range of 1 to 7 indicating the minimum number + * of days required in the first week of the year, e.g. 1 for en-US, + * 4 for de + * weekend + * an array with values in the range 1=Monday to 7=Sunday indicating the + * days of the week considered as part of the weekend, e.g. [6, 7] for en-US + * and en-GB, [7] for bn-IN (note that "weekend" is *not* necessarily two + * days) + * + * NOTE: "calendar" and "locale" properties are *not* added to the object. + */ +[[nodiscard]] extern bool intl_GetCalendarInfo(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * Compares a BCP 47 language tag against the locales in availableLocales and + * returns the best available match -- or |undefined| if no match was found. + * Uses the fallback mechanism of RFC 4647, section 3.4. + * + * The set of available locales consulted doesn't necessarily include the + * default locale or any generalized forms of it (e.g. "de" is a more-general + * form of "de-CH"). If you want to be sure to consider the default local and + * its generalized forms (you usually will), pass the default locale as the + * value of |defaultOrNull|; otherwise pass null. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.2. + * Spec: RFC 4647, section 3.4. + * + * Usage: result = intl_BestAvailableLocale("Collator", locale, defaultOrNull) + */ +[[nodiscard]] extern bool intl_BestAvailableLocale(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * Return the supported locale for the input locale if ICU supports that locale + * (perhaps via fallback, e.g. supporting "de-CH" through "de" support implied + * by a "de-DE" locale). Otherwise uses the last-ditch locale. + * + * Usage: result = intl_supportedLocaleOrFallback(locale) + */ +[[nodiscard]] extern bool intl_supportedLocaleOrFallback(JSContext* cx, + unsigned argc, + JS::Value* vp); + +/** + * Returns the list of supported values for the given key. Throws a RangeError + * if the key isn't one of {"calendar", "collation", "currency", + * "numberingSystem", "timeZone", "unit"}. + * + * Usage: list = intl_SupportedValuesOf(key) + */ +[[nodiscard]] extern bool intl_SupportedValuesOf(JSContext* cx, unsigned argc, + JS::Value* vp); + +} // namespace js + +#endif /* builtin_intl_IntlObject_h */ diff --git a/js/src/builtin/intl/IntlObject.js b/js/src/builtin/intl/IntlObject.js new file mode 100644 index 0000000000..95b158bb27 --- /dev/null +++ b/js/src/builtin/intl/IntlObject.js @@ -0,0 +1,81 @@ +/* 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/. */ + +/** + * 8.2.1 Intl.getCanonicalLocales ( locales ) + * + * ES2017 Intl draft rev 947aa9a0c853422824a0c9510d8f09be3eb416b9 + */ +function Intl_getCanonicalLocales(locales) { + // Steps 1-2. + return CanonicalizeLocaleList(locales); +} + +/** + * Intl.supportedValuesOf ( key ) + */ +function Intl_supportedValuesOf(key) { + // Step 1. + key = ToString(key); + + // Steps 2-9. + return intl_SupportedValuesOf(key); +} + +/** + * This function is a custom function in the style of the standard Intl.* + * functions, that isn't part of any spec or proposal yet. + * + * Returns an object with the following properties: + * locale: + * The actual resolved locale. + * + * calendar: + * The default calendar of the resolved locale. + * + * firstDayOfWeek: + * The first day of the week for the resolved locale. + * + * minDays: + * The minimum number of days in a week for the resolved locale. + * + * weekend: + * The days of the week considered as the weekend for the resolved locale. + * + * Days are encoded as integers in the range 1=Monday to 7=Sunday. + */ +function Intl_getCalendarInfo(locales) { + // 1. Let requestLocales be ? CanonicalizeLocaleList(locales). + const requestedLocales = CanonicalizeLocaleList(locales); + + const DateTimeFormat = dateTimeFormatInternalProperties; + + // 2. Let localeData be %DateTimeFormat%.[[localeData]]. + const localeData = DateTimeFormat.localeData; + + // 3. Let localeOpt be a new Record. + const localeOpt = new_Record(); + + // 4. Set localeOpt.[[localeMatcher]] to "best fit". + localeOpt.localeMatcher = "best fit"; + + // 5. Let r be ResolveLocale(%DateTimeFormat%.[[availableLocales]], + // requestedLocales, localeOpt, + // %DateTimeFormat%.[[relevantExtensionKeys]], localeData). + const r = ResolveLocale( + "DateTimeFormat", + requestedLocales, + localeOpt, + DateTimeFormat.relevantExtensionKeys, + localeData + ); + + // 6. Let result be GetCalendarInfo(r.[[locale]]). + const result = intl_GetCalendarInfo(r.locale); + DefineDataProperty(result, "calendar", r.ca); + DefineDataProperty(result, "locale", r.locale); + + // 7. Return result. + return result; +} diff --git a/js/src/builtin/intl/LanguageTag.cpp b/js/src/builtin/intl/LanguageTag.cpp new file mode 100644 index 0000000000..3372f5d99a --- /dev/null +++ b/js/src/builtin/intl/LanguageTag.cpp @@ -0,0 +1,193 @@ +/* -*- 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 "builtin/intl/LanguageTag.h" + +#include "mozilla/intl/Locale.h" +#include "mozilla/Span.h" + +#include "builtin/intl/StringAsciiChars.h" +#include "gc/Tracer.h" +#include "vm/JSContext.h" + +namespace js { +namespace intl { + +[[nodiscard]] bool ParseLocale(JSContext* cx, Handle<JSLinearString*> str, + mozilla::intl::Locale& result) { + if (StringIsAscii(str)) { + intl::StringAsciiChars chars(str); + if (!chars.init(cx)) { + return false; + } + + if (mozilla::intl::LocaleParser::TryParse(chars, result).isOk()) { + return true; + } + } + + if (UniqueChars localeChars = QuoteString(cx, str, '"')) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_LANGUAGE_TAG, localeChars.get()); + } + return false; +} + +bool ParseStandaloneLanguageTag(Handle<JSLinearString*> str, + mozilla::intl::LanguageSubtag& result) { + // Tell the analysis the |IsStructurallyValidLanguageTag| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + if (str->hasLatin1Chars()) { + if (!mozilla::intl::IsStructurallyValidLanguageTag<Latin1Char>( + str->latin1Range(nogc))) { + return false; + } + result.Set<Latin1Char>(str->latin1Range(nogc)); + } else { + if (!mozilla::intl::IsStructurallyValidLanguageTag<char16_t>( + str->twoByteRange(nogc))) { + return false; + } + result.Set<char16_t>(str->twoByteRange(nogc)); + } + return true; +} + +bool ParseStandaloneScriptTag(Handle<JSLinearString*> str, + mozilla::intl::ScriptSubtag& result) { + // Tell the analysis the |IsStructurallyValidScriptTag| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + if (str->hasLatin1Chars()) { + if (!mozilla::intl::IsStructurallyValidScriptTag<Latin1Char>( + str->latin1Range(nogc))) { + return false; + } + result.Set<Latin1Char>(str->latin1Range(nogc)); + } else { + if (!mozilla::intl::IsStructurallyValidScriptTag<char16_t>( + str->twoByteRange(nogc))) { + return false; + } + result.Set<char16_t>(str->twoByteRange(nogc)); + } + return true; +} + +bool ParseStandaloneRegionTag(Handle<JSLinearString*> str, + mozilla::intl::RegionSubtag& result) { + // Tell the analysis the |IsStructurallyValidRegionTag| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + if (str->hasLatin1Chars()) { + if (!mozilla::intl::IsStructurallyValidRegionTag<Latin1Char>( + str->latin1Range(nogc))) { + return false; + } + result.Set<Latin1Char>(str->latin1Range(nogc)); + } else { + if (!mozilla::intl::IsStructurallyValidRegionTag<char16_t>( + str->twoByteRange(nogc))) { + return false; + } + result.Set<char16_t>(str->twoByteRange(nogc)); + } + return true; +} + +template <typename CharT> +static bool IsAsciiLowercaseAlpha(mozilla::Span<const CharT> span) { + // Tell the analysis the |std::all_of| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + const CharT* ptr = span.data(); + size_t length = span.size(); + return std::all_of(ptr, ptr + length, mozilla::IsAsciiLowercaseAlpha<CharT>); +} + +static bool IsAsciiLowercaseAlpha(JSLinearString* str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return IsAsciiLowercaseAlpha<Latin1Char>(str->latin1Range(nogc)); + } + return IsAsciiLowercaseAlpha<char16_t>(str->twoByteRange(nogc)); +} + +template <typename CharT> +static bool IsAsciiAlpha(mozilla::Span<const CharT> span) { + // Tell the analysis the |std::all_of| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + const CharT* ptr = span.data(); + size_t length = span.size(); + return std::all_of(ptr, ptr + length, mozilla::IsAsciiAlpha<CharT>); +} + +static bool IsAsciiAlpha(JSLinearString* str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return IsAsciiAlpha<Latin1Char>(str->latin1Range(nogc)); + } + return IsAsciiAlpha<char16_t>(str->twoByteRange(nogc)); +} + +JS::Result<JSString*> ParseStandaloneISO639LanguageTag( + JSContext* cx, Handle<JSLinearString*> str) { + // ISO-639 language codes contain either two or three characters. + size_t length = str->length(); + if (length != 2 && length != 3) { + return nullptr; + } + + // We can directly the return the input below if it's in the correct case. + bool isLowerCase = IsAsciiLowercaseAlpha(str); + if (!isLowerCase) { + // Must be an ASCII alpha string. + if (!IsAsciiAlpha(str)) { + return nullptr; + } + } + + mozilla::intl::LanguageSubtag languageTag; + if (str->hasLatin1Chars()) { + JS::AutoCheckCannotGC nogc; + languageTag.Set<Latin1Char>(str->latin1Range(nogc)); + } else { + JS::AutoCheckCannotGC nogc; + languageTag.Set<char16_t>(str->twoByteRange(nogc)); + } + + if (!isLowerCase) { + // The language subtag is canonicalized to lower case. + languageTag.ToLowerCase(); + } + + // Reject the input if the canonical tag contains more than just a single + // language subtag. + if (mozilla::intl::Locale::ComplexLanguageMapping(languageTag)) { + return nullptr; + } + + // Take care to replace deprecated subtags with their preferred values. + JSString* result; + if (mozilla::intl::Locale::LanguageMapping(languageTag) || !isLowerCase) { + result = NewStringCopy<CanGC>(cx, languageTag.Span()); + } else { + result = str; + } + if (!result) { + return cx->alreadyReportedOOM(); + } + return result; +} + +void js::intl::UnicodeExtensionKeyword::trace(JSTracer* trc) { + TraceRoot(trc, &type_, "UnicodeExtensionKeyword::type"); +} + +} // namespace intl +} // namespace js diff --git a/js/src/builtin/intl/LanguageTag.h b/js/src/builtin/intl/LanguageTag.h new file mode 100644 index 0000000000..e896411e19 --- /dev/null +++ b/js/src/builtin/intl/LanguageTag.h @@ -0,0 +1,91 @@ +/* -*- 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/. */ + +/* Structured representation of Unicode locale IDs used with Intl functions. */ + +#ifndef builtin_intl_LanguageTag_h +#define builtin_intl_LanguageTag_h + +#include "mozilla/intl/Locale.h" +#include "mozilla/Span.h" + +#include "js/Result.h" +#include "js/RootingAPI.h" + +struct JS_PUBLIC_API JSContext; +class JSLinearString; +class JS_PUBLIC_API JSString; +class JS_PUBLIC_API JSTracer; + +namespace js { + +namespace intl { + +/** + * Parse a string Unicode BCP 47 locale identifier. If successful, store in + * |result| and return true. Otherwise return false. + */ +[[nodiscard]] bool ParseLocale(JSContext* cx, JS::Handle<JSLinearString*> str, + mozilla::intl::Locale& result); + +/** + * Parse a string as a standalone |language| tag. If |str| is a standalone + * language tag, store it in |result| and return true. Otherwise return false. + */ +[[nodiscard]] bool ParseStandaloneLanguageTag( + JS::Handle<JSLinearString*> str, mozilla::intl::LanguageSubtag& result); + +/** + * Parse a string as a standalone |script| tag. If |str| is a standalone script + * tag, store it in |result| and return true. Otherwise return false. + */ +[[nodiscard]] bool ParseStandaloneScriptTag( + JS::Handle<JSLinearString*> str, mozilla::intl::ScriptSubtag& result); + +/** + * Parse a string as a standalone |region| tag. If |str| is a standalone region + * tag, store it in |result| and return true. Otherwise return false. + */ +[[nodiscard]] bool ParseStandaloneRegionTag( + JS::Handle<JSLinearString*> str, mozilla::intl::RegionSubtag& result); + +/** + * Parse a string as an ISO-639 language code. Return |nullptr| in the result if + * the input could not be parsed or the canonical form of the resulting language + * tag contains more than a single language subtag. + */ +JS::Result<JSString*> ParseStandaloneISO639LanguageTag( + JSContext* cx, JS::Handle<JSLinearString*> str); + +class UnicodeExtensionKeyword final { + char key_[mozilla::intl::LanguageTagLimits::UnicodeKeyLength]; + JSLinearString* type_; + + public: + using UnicodeKey = + const char (&)[mozilla::intl::LanguageTagLimits::UnicodeKeyLength + 1]; + using UnicodeKeySpan = + mozilla::Span<const char, + mozilla::intl::LanguageTagLimits::UnicodeKeyLength>; + + UnicodeExtensionKeyword(UnicodeKey key, JSLinearString* type) + : key_{key[0], key[1]}, type_(type) {} + + UnicodeKeySpan key() const { return {key_, sizeof(key_)}; } + JSLinearString* type() const { return type_; } + + void trace(JSTracer* trc); +}; + +[[nodiscard]] extern bool ApplyUnicodeExtensionToTag( + JSContext* cx, mozilla::intl::Locale& tag, + JS::HandleVector<UnicodeExtensionKeyword> keywords); + +} // namespace intl + +} // namespace js + +#endif /* builtin_intl_LanguageTag_h */ diff --git a/js/src/builtin/intl/ListFormat.cpp b/js/src/builtin/intl/ListFormat.cpp new file mode 100644 index 0000000000..7d11d5acd6 --- /dev/null +++ b/js/src/builtin/intl/ListFormat.cpp @@ -0,0 +1,373 @@ +/* -*- 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 "builtin/intl/ListFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/intl/ListFormat.h" + +#include <stddef.h> + +#include "builtin/Array.h" +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/FormatBuffer.h" +#include "gc/GCContext.h" +#include "js/Utility.h" +#include "js/Vector.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/StringType.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" +#include "vm/ObjectOperations-inl.h" + +using namespace js; + +const JSClassOps ListFormatObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + ListFormatObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; +const JSClass ListFormatObject::class_ = { + "Intl.ListFormat", + JSCLASS_HAS_RESERVED_SLOTS(ListFormatObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_ListFormat) | + JSCLASS_FOREGROUND_FINALIZE, + &ListFormatObject::classOps_, &ListFormatObject::classSpec_}; + +const JSClass& ListFormatObject::protoClass_ = PlainObject::class_; + +static bool listFormat_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().ListFormat); + return true; +} + +static const JSFunctionSpec listFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", + "Intl_ListFormat_supportedLocalesOf", 1, 0), + JS_FS_END}; + +static const JSFunctionSpec listFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_ListFormat_resolvedOptions", 0, + 0), + JS_SELF_HOSTED_FN("format", "Intl_ListFormat_format", 1, 0), + JS_SELF_HOSTED_FN("formatToParts", "Intl_ListFormat_formatToParts", 1, 0), + JS_FN(js_toSource_str, listFormat_toSource, 0, 0), JS_FS_END}; + +static const JSPropertySpec listFormat_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Intl.ListFormat", JSPROP_READONLY), + JS_PS_END}; + +static bool ListFormat(JSContext* cx, unsigned argc, Value* vp); + +const ClassSpec ListFormatObject::classSpec_ = { + GenericCreateConstructor<ListFormat, 0, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<ListFormatObject>, + listFormat_static_methods, + nullptr, + listFormat_methods, + listFormat_properties, + nullptr, + ClassSpec::DontDefineConstructor}; + +/** + * Intl.ListFormat([ locales [, options]]) + */ +static bool ListFormat(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Intl.ListFormat")) { + return false; + } + + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_ListFormat, + &proto)) { + return false; + } + + Rooted<ListFormatObject*> listFormat( + cx, NewObjectWithClassProto<ListFormatObject>(cx, proto)); + if (!listFormat) { + return false; + } + + HandleValue locales = args.get(0); + HandleValue options = args.get(1); + + // Step 3. + if (!intl::InitializeObject(cx, listFormat, cx->names().InitializeListFormat, + locales, options)) { + return false; + } + + args.rval().setObject(*listFormat); + return true; +} + +void js::ListFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + + mozilla::intl::ListFormat* lf = + obj->as<ListFormatObject>().getListFormatSlot(); + if (lf) { + intl::RemoveICUCellMemory(gcx, obj, ListFormatObject::EstimatedMemoryUse); + delete lf; + } +} + +/** + * Returns a new ListFormat with the locale and list formatting options + * of the given ListFormat. + */ +static mozilla::intl::ListFormat* NewListFormat( + JSContext* cx, Handle<ListFormatObject*> listFormat) { + RootedObject internals(cx, intl::GetInternalsObject(cx, listFormat)); + if (!internals) { + return nullptr; + } + + RootedValue value(cx); + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) { + return nullptr; + } + UniqueChars locale = intl::EncodeLocale(cx, value.toString()); + if (!locale) { + return nullptr; + } + + mozilla::intl::ListFormat::Options options; + + using ListFormatType = mozilla::intl::ListFormat::Type; + if (!GetProperty(cx, internals, internals, cx->names().type, &value)) { + return nullptr; + } + { + JSLinearString* strType = value.toString()->ensureLinear(cx); + if (!strType) { + return nullptr; + } + + if (StringEqualsLiteral(strType, "conjunction")) { + options.mType = ListFormatType::Conjunction; + } else if (StringEqualsLiteral(strType, "disjunction")) { + options.mType = ListFormatType::Disjunction; + } else { + MOZ_ASSERT(StringEqualsLiteral(strType, "unit")); + options.mType = ListFormatType::Unit; + } + } + + using ListFormatStyle = mozilla::intl::ListFormat::Style; + if (!GetProperty(cx, internals, internals, cx->names().style, &value)) { + return nullptr; + } + { + JSLinearString* strStyle = value.toString()->ensureLinear(cx); + if (!strStyle) { + return nullptr; + } + + if (StringEqualsLiteral(strStyle, "long")) { + options.mStyle = ListFormatStyle::Long; + } else if (StringEqualsLiteral(strStyle, "short")) { + options.mStyle = ListFormatStyle::Short; + } else { + MOZ_ASSERT(StringEqualsLiteral(strStyle, "narrow")); + options.mStyle = ListFormatStyle::Narrow; + } + } + + auto result = mozilla::intl::ListFormat::TryCreate( + mozilla::MakeStringSpan(locale.get()), options); + + if (result.isOk()) { + return result.unwrap().release(); + } + + js::intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; +} + +static mozilla::intl::ListFormat* GetOrCreateListFormat( + JSContext* cx, Handle<ListFormatObject*> listFormat) { + // Obtain a cached mozilla::intl::ListFormat object. + mozilla::intl::ListFormat* lf = listFormat->getListFormatSlot(); + if (lf) { + return lf; + } + + lf = NewListFormat(cx, listFormat); + if (!lf) { + return nullptr; + } + listFormat->setListFormatSlot(lf); + + intl::AddICUCellMemory(listFormat, ListFormatObject::EstimatedMemoryUse); + return lf; +} + +/** + * FormatList ( listFormat, list ) + */ +static bool FormatList(JSContext* cx, mozilla::intl::ListFormat* lf, + const mozilla::intl::ListFormat::StringList& list, + MutableHandleValue result) { + intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> formatBuffer(cx); + auto formatResult = lf->Format(list, formatBuffer); + if (formatResult.isErr()) { + js::intl::ReportInternalError(cx, formatResult.unwrapErr()); + return false; + } + + JSString* str = formatBuffer.toString(cx); + if (!str) { + return false; + } + result.setString(str); + return true; +} + +/** + * FormatListToParts ( listFormat, list ) + */ +static bool FormatListToParts(JSContext* cx, mozilla::intl::ListFormat* lf, + const mozilla::intl::ListFormat::StringList& list, + MutableHandleValue result) { + intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + mozilla::intl::ListFormat::PartVector parts; + auto formatResult = lf->FormatToParts(list, buffer, parts); + if (formatResult.isErr()) { + intl::ReportInternalError(cx, formatResult.unwrapErr()); + return false; + } + + RootedString overallResult(cx, buffer.toString(cx)); + if (!overallResult) { + return false; + } + + Rooted<ArrayObject*> partsArray( + cx, NewDenseFullyAllocatedArray(cx, parts.length())); + if (!partsArray) { + return false; + } + partsArray->ensureDenseInitializedLength(0, parts.length()); + + RootedObject singlePart(cx); + RootedValue val(cx); + + size_t index = 0; + size_t beginIndex = 0; + for (const mozilla::intl::ListFormat::Part& part : parts) { + singlePart = NewPlainObject(cx); + if (!singlePart) { + return false; + } + + if (part.first == mozilla::intl::ListFormat::PartType::Element) { + val = StringValue(cx->names().element); + } else { + val = StringValue(cx->names().literal); + } + + if (!DefineDataProperty(cx, singlePart, cx->names().type, val)) { + return false; + } + + // There could be an empty string so the endIndex coule be equal to + // beginIndex. + MOZ_ASSERT(part.second >= beginIndex); + JSLinearString* partStr = NewDependentString(cx, overallResult, beginIndex, + part.second - beginIndex); + if (!partStr) { + return false; + } + val = StringValue(partStr); + if (!DefineDataProperty(cx, singlePart, cx->names().value, val)) { + return false; + } + + beginIndex = part.second; + partsArray->initDenseElement(index++, ObjectValue(*singlePart)); + } + + MOZ_ASSERT(index == parts.length()); + MOZ_ASSERT(beginIndex == buffer.length()); + result.setObject(*partsArray); + + return true; +} + +bool js::intl_FormatList(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + + Rooted<ListFormatObject*> listFormat( + cx, &args[0].toObject().as<ListFormatObject>()); + + bool formatToParts = args[2].toBoolean(); + + mozilla::intl::ListFormat* lf = GetOrCreateListFormat(cx, listFormat); + if (!lf) { + return false; + } + + // Collect all strings and their lengths. + // + // 'strings' takes the ownership of those strings, and 'list' will be passed + // to mozilla::intl::ListFormat as a Span. + Vector<UniqueTwoByteChars, mozilla::intl::DEFAULT_LIST_LENGTH> strings(cx); + mozilla::intl::ListFormat::StringList list; + + Rooted<ArrayObject*> listObj(cx, &args[1].toObject().as<ArrayObject>()); + RootedValue value(cx); + uint32_t listLen = listObj->length(); + for (uint32_t i = 0; i < listLen; i++) { + if (!GetElement(cx, listObj, listObj, i, &value)) { + return false; + } + + JSLinearString* linear = value.toString()->ensureLinear(cx); + if (!linear) { + return false; + } + + size_t linearLength = linear->length(); + + UniqueTwoByteChars chars = cx->make_pod_array<char16_t>(linearLength); + if (!chars) { + return false; + } + CopyChars(chars.get(), *linear); + + if (!strings.append(std::move(chars))) { + return false; + } + + if (!list.emplaceBack(strings[i].get(), linearLength)) { + return false; + } + } + + if (formatToParts) { + return FormatListToParts(cx, lf, list, args.rval()); + } + return FormatList(cx, lf, list, args.rval()); +} diff --git a/js/src/builtin/intl/ListFormat.h b/js/src/builtin/intl/ListFormat.h new file mode 100644 index 0000000000..da0daa711b --- /dev/null +++ b/js/src/builtin/intl/ListFormat.h @@ -0,0 +1,69 @@ +/* -*- 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 builtin_intl_ListFormat_h +#define builtin_intl_ListFormat_h + +#include <stdint.h> + +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "js/TypeDecls.h" +#include "vm/NativeObject.h" + +namespace mozilla::intl { +class ListFormat; +} // namespace mozilla::intl + +namespace js { + +class ListFormatObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t LIST_FORMAT_SLOT = 1; + static constexpr uint32_t SLOT_COUNT = 2; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals " + "object slot"); + + // Estimated memory use for UListFormatter (see IcuMemoryUsage). + static constexpr size_t EstimatedMemoryUse = 24; + + mozilla::intl::ListFormat* getListFormatSlot() const { + const auto& slot = getFixedSlot(LIST_FORMAT_SLOT); + if (slot.isUndefined()) { + return nullptr; + } + return static_cast<mozilla::intl::ListFormat*>(slot.toPrivate()); + } + + void setListFormatSlot(mozilla::intl::ListFormat* format) { + setFixedSlot(LIST_FORMAT_SLOT, PrivateValue(format)); + } + + private: + static const JSClassOps classOps_; + static const ClassSpec classSpec_; + + static void finalize(JS::GCContext* gcx, JSObject* obj); +}; + +/** + * Returns a string representing the array of string values |list| according to + * the effective locale and the formatting options of the given ListFormat. + * + * Usage: formatted = intl_FormatList(listFormat, list, formatToParts) + */ +[[nodiscard]] extern bool intl_FormatList(JSContext* cx, unsigned argc, + Value* vp); + +} // namespace js + +#endif /* builtin_intl_ListFormat_h */ diff --git a/js/src/builtin/intl/ListFormat.js b/js/src/builtin/intl/ListFormat.js new file mode 100644 index 0000000000..463c669a44 --- /dev/null +++ b/js/src/builtin/intl/ListFormat.js @@ -0,0 +1,330 @@ +/* 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/. */ + +/** + * ListFormat internal properties. + */ +function listFormatLocaleData() { + // ListFormat don't support any extension keys. + return {}; +} +var listFormatInternalProperties = { + localeData: listFormatLocaleData, + relevantExtensionKeys: [], +}; + +/** + * Intl.ListFormat ( [ locales [ , options ] ] ) + * + * Compute an internal properties object from |lazyListFormatData|. + */ +function resolveListFormatInternals(lazyListFormatData) { + assert(IsObject(lazyListFormatData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + var ListFormat = listFormatInternalProperties; + + // Compute effective locale. + + // Step 9. + var localeData = ListFormat.localeData; + + // Step 10. + var r = ResolveLocale( + "ListFormat", + lazyListFormatData.requestedLocales, + lazyListFormatData.opt, + ListFormat.relevantExtensionKeys, + localeData + ); + + // Step 11. + internalProps.locale = r.locale; + + // Step 13. + internalProps.type = lazyListFormatData.type; + + // Step 15. + internalProps.style = lazyListFormatData.style; + + // Steps 16-23 (not applicable in our implementation). + + // The caller is responsible for associating |internalProps| with the right + // object using |setInternalProperties|. + return internalProps; +} + +/** + * Returns an object containing the ListFormat internal properties of |obj|. + */ +function getListFormatInternals(obj) { + assert(IsObject(obj), "getListFormatInternals called with non-object"); + assert( + intl_GuardToListFormat(obj) !== null, + "getListFormatInternals called with non-ListFormat" + ); + + var internals = getIntlObjectInternals(obj); + assert( + internals.type === "ListFormat", + "bad type escaped getIntlObjectInternals" + ); + + // If internal properties have already been computed, use them. + var internalProps = maybeInternalProperties(internals); + if (internalProps) { + return internalProps; + } + + // Otherwise it's time to fully create them. + internalProps = resolveListFormatInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * Intl.ListFormat ( [ locales [ , options ] ] ) + * + * Initializes an object as a ListFormat. + * + * This method is complicated a moderate bit by its implementing initialization + * as a *lazy* concept. Everything that must happen now, does -- but we defer + * all the work we can until the object is actually used as a ListFormat. + * This later work occurs in |resolveListFormatInternals|; steps not noted + * here occur there. + */ +function InitializeListFormat(listFormat, locales, options) { + assert(IsObject(listFormat), "InitializeListFormat called with non-object"); + assert( + intl_GuardToListFormat(listFormat) !== null, + "InitializeListFormat called with non-ListFormat" + ); + + // Lazy ListFormat data has the following structure: + // + // { + // requestedLocales: List of locales, + // type: "conjunction" / "disjunction" / "unit", + // style: "long" / "short" / "narrow", + // + // opt: // opt object computed in InitializeListFormat + // { + // localeMatcher: "lookup" / "best fit", + // } + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every ListFormat lazy data object has *all* these properties, never a + // subset of them. + var lazyListFormatData = std_Object_create(null); + + // Step 3. + var requestedLocales = CanonicalizeLocaleList(locales); + lazyListFormatData.requestedLocales = requestedLocales; + + // Steps 4-5. + if (options === undefined) { + options = std_Object_create(null); + } else if (!IsObject(options)) { + ThrowTypeError( + JSMSG_OBJECT_REQUIRED, + options === null ? "null" : typeof options + ); + } + + // Step 6. + var opt = new_Record(); + lazyListFormatData.opt = opt; + + // Steps 7-8. + let matcher = GetOption( + options, + "localeMatcher", + "string", + ["lookup", "best fit"], + "best fit" + ); + opt.localeMatcher = matcher; + + // Compute formatting options. + + // Steps 12-13. + var type = GetOption( + options, + "type", + "string", + ["conjunction", "disjunction", "unit"], + "conjunction" + ); + lazyListFormatData.type = type; + + // Steps 14-15. + var style = GetOption( + options, + "style", + "string", + ["long", "short", "narrow"], + "long" + ); + lazyListFormatData.style = style; + + // We've done everything that must be done now: mark the lazy data as fully + // computed and install it. + initializeIntlObject(listFormat, "ListFormat", lazyListFormatData); +} + +/** + * Returns the subset of the given locale list for which this locale list has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + */ +function Intl_ListFormat_supportedLocalesOf(locales /*, options*/) { + var options = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 1. + var availableLocales = "ListFormat"; + + // Step 2. + var requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. + return SupportedLocales(availableLocales, requestedLocales, options); +} + +/** + * StringListFromIterable ( iterable ) + */ +function StringListFromIterable(iterable, methodName) { + // Step 1. + if (iterable === undefined) { + return []; + } + + // Step 3. + var list = []; + + // Steps 2, 4-5. + for (var element of allowContentIter(iterable)) { + // Step 5.b.ii. + if (typeof element !== "string") { + ThrowTypeError( + JSMSG_NOT_EXPECTED_TYPE, + methodName, + "string", + typeof element + ); + } + + // Step 5.b.iii. + DefineDataProperty(list, list.length, element); + } + + // Step 6. + return list; +} + +/** + * Intl.ListFormat.prototype.format ( list ) + */ +function Intl_ListFormat_format(list) { + // Step 1. + var listFormat = this; + + // Steps 2-3. + if ( + !IsObject(listFormat) || + (listFormat = intl_GuardToListFormat(listFormat)) === null + ) { + return callFunction( + intl_CallListFormatMethodIfWrapped, + this, + list, + "Intl_ListFormat_format" + ); + } + + // Step 4. + var stringList = StringListFromIterable(list, "format"); + + // We can directly return if |stringList| contains less than two elements. + if (stringList.length < 2) { + return stringList.length === 0 ? "" : stringList[0]; + } + + // Ensure the ListFormat internals are resolved. + getListFormatInternals(listFormat); + + // Step 5. + return intl_FormatList(listFormat, stringList, /* formatToParts = */ false); +} + +/** + * Intl.ListFormat.prototype.formatToParts ( list ) + */ +function Intl_ListFormat_formatToParts(list) { + // Step 1. + var listFormat = this; + + // Steps 2-3. + if ( + !IsObject(listFormat) || + (listFormat = intl_GuardToListFormat(listFormat)) === null + ) { + return callFunction( + intl_CallListFormatMethodIfWrapped, + this, + list, + "Intl_ListFormat_formatToParts" + ); + } + + // Step 4. + var stringList = StringListFromIterable(list, "formatToParts"); + + // We can directly return if |stringList| contains less than two elements. + if (stringList.length < 2) { + return stringList.length === 0 + ? [] + : [{ type: "element", value: stringList[0] }]; + } + + // Ensure the ListFormat internals are resolved. + getListFormatInternals(listFormat); + + // Step 5. + return intl_FormatList(listFormat, stringList, /* formatToParts = */ true); +} + +/** + * Returns the resolved options for a ListFormat object. + */ +function Intl_ListFormat_resolvedOptions() { + // Step 1. + var listFormat = this; + + // Steps 2-3. + if ( + !IsObject(listFormat) || + (listFormat = intl_GuardToListFormat(listFormat)) === null + ) { + return callFunction( + intl_CallListFormatMethodIfWrapped, + this, + "Intl_ListFormat_resolvedOptions" + ); + } + + var internals = getListFormatInternals(listFormat); + + // Steps 4-5. + var result = { + locale: internals.locale, + type: internals.type, + style: internals.style, + }; + + // Step 6. + return result; +} diff --git a/js/src/builtin/intl/Locale.cpp b/js/src/builtin/intl/Locale.cpp new file mode 100644 index 0000000000..b30def7f99 --- /dev/null +++ b/js/src/builtin/intl/Locale.cpp @@ -0,0 +1,1517 @@ +/* -*- 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/. */ + +/* Intl.Locale implementation. */ + +#include "builtin/intl/Locale.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/intl/Locale.h" +#include "mozilla/Maybe.h" +#include "mozilla/Span.h" +#include "mozilla/TextUtils.h" + +#include <algorithm> +#include <string> +#include <string.h> +#include <utility> + +#include "builtin/Boolean.h" +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/FormatBuffer.h" +#include "builtin/intl/LanguageTag.h" +#include "builtin/intl/StringAsciiChars.h" +#include "builtin/String.h" +#include "js/Conversions.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/Printer.h" +#include "js/TypeDecls.h" +#include "js/Wrapper.h" +#include "vm/Compartment.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/StringType.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; +using namespace mozilla::intl::LanguageTagLimits; + +const JSClass LocaleObject::class_ = { + "Intl.Locale", + JSCLASS_HAS_RESERVED_SLOTS(LocaleObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_Locale), + JS_NULL_CLASS_OPS, &LocaleObject::classSpec_}; + +const JSClass& LocaleObject::protoClass_ = PlainObject::class_; + +static inline bool IsLocale(HandleValue v) { + return v.isObject() && v.toObject().is<LocaleObject>(); +} + +// Return the length of the base-name subtags. +static size_t BaseNameLength(const mozilla::intl::Locale& tag) { + size_t baseNameLength = tag.Language().Length(); + if (tag.Script().Present()) { + baseNameLength += 1 + tag.Script().Length(); + } + if (tag.Region().Present()) { + baseNameLength += 1 + tag.Region().Length(); + } + for (const auto& variant : tag.Variants()) { + baseNameLength += 1 + variant.size(); + } + return baseNameLength; +} + +struct IndexAndLength { + size_t index; + size_t length; + + IndexAndLength(size_t index, size_t length) : index(index), length(length){}; + + template <typename T> + mozilla::Span<const T> spanOf(const T* ptr) const { + return {ptr + index, length}; + } +}; + +// Compute the Unicode extension's index and length in the extension subtag. +static mozilla::Maybe<IndexAndLength> UnicodeExtensionPosition( + const mozilla::intl::Locale& tag) { + size_t index = 0; + for (const auto& extension : tag.Extensions()) { + MOZ_ASSERT(!mozilla::IsAsciiUppercaseAlpha(extension[0]), + "extensions are case normalized to lowercase"); + + size_t extensionLength = extension.size(); + if (extension[0] == 'u') { + return mozilla::Some(IndexAndLength{index, extensionLength}); + } + + // Add +1 to skip over the preceding separator. + index += 1 + extensionLength; + } + return mozilla::Nothing(); +} + +static LocaleObject* CreateLocaleObject(JSContext* cx, HandleObject prototype, + const mozilla::intl::Locale& tag) { + intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + if (auto result = tag.ToString(buffer); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + + RootedString tagStr(cx, buffer.toAsciiString(cx)); + if (!tagStr) { + return nullptr; + } + + size_t baseNameLength = BaseNameLength(tag); + + RootedString baseName(cx, NewDependentString(cx, tagStr, 0, baseNameLength)); + if (!baseName) { + return nullptr; + } + + RootedValue unicodeExtension(cx, UndefinedValue()); + if (auto result = UnicodeExtensionPosition(tag)) { + JSString* str = NewDependentString( + cx, tagStr, baseNameLength + 1 + result->index, result->length); + if (!str) { + return nullptr; + } + + unicodeExtension.setString(str); + } + + auto* locale = NewObjectWithClassProto<LocaleObject>(cx, prototype); + if (!locale) { + return nullptr; + } + + locale->setFixedSlot(LocaleObject::LANGUAGE_TAG_SLOT, StringValue(tagStr)); + locale->setFixedSlot(LocaleObject::BASENAME_SLOT, StringValue(baseName)); + locale->setFixedSlot(LocaleObject::UNICODE_EXTENSION_SLOT, unicodeExtension); + + return locale; +} + +static inline bool IsValidUnicodeExtensionValue(JSContext* cx, + JSLinearString* linear, + bool* isValid) { + if (linear->length() == 0) { + *isValid = false; + return true; + } + + if (!StringIsAscii(linear)) { + *isValid = false; + return true; + } + + intl::StringAsciiChars chars(linear); + if (!chars.init(cx)) { + return false; + } + + *isValid = + mozilla::intl::LocaleParser::CanParseUnicodeExtensionType(chars).isOk(); + return true; +} + +/** Iterate through (sep keyword) in a valid, lowercased Unicode extension. */ +template <typename CharT> +class SepKeywordIterator { + const CharT* iter_; + const CharT* const end_; + + public: + SepKeywordIterator(const CharT* unicodeExtensionBegin, + const CharT* unicodeExtensionEnd) + : iter_(unicodeExtensionBegin), end_(unicodeExtensionEnd) {} + + /** + * Return (sep keyword) in the Unicode locale extension from begin to end. + * The first call after all (sep keyword) are consumed returns |nullptr|; no + * further calls are allowed. + */ + const CharT* next() { + MOZ_ASSERT(iter_ != nullptr, + "can't call next() once it's returned nullptr"); + + constexpr size_t SepKeyLength = 1 + UnicodeKeyLength; // "-co"/"-nu"/etc. + + MOZ_ASSERT(iter_ + SepKeyLength <= end_, + "overall Unicode locale extension or non-leading subtags must " + "be at least key-sized"); + + MOZ_ASSERT((iter_[0] == 'u' && iter_[1] == '-') || iter_[0] == '-'); + + while (true) { + // Skip past '-' so |std::char_traits::find| makes progress. Skipping + // 'u' is harmless -- skip or not, |find| returns the first '-'. + iter_++; + + // Find the next separator. + iter_ = std::char_traits<CharT>::find( + iter_, mozilla::PointerRangeSize(iter_, end_), CharT('-')); + if (!iter_) { + return nullptr; + } + + MOZ_ASSERT(iter_ + SepKeyLength <= end_, + "non-leading subtags in a Unicode locale extension are all " + "at least as long as a key"); + + if (iter_ + SepKeyLength == end_ || // key is terminal subtag + iter_[SepKeyLength] == '-') { // key is followed by more subtags + break; + } + } + + MOZ_ASSERT(iter_[0] == '-'); + MOZ_ASSERT(mozilla::IsAsciiLowercaseAlpha(iter_[1]) || + mozilla::IsAsciiDigit(iter_[1])); + MOZ_ASSERT(mozilla::IsAsciiLowercaseAlpha(iter_[2])); + MOZ_ASSERT_IF(iter_ + SepKeyLength < end_, iter_[SepKeyLength] == '-'); + return iter_; + } +}; + +/** + * 9.2.10 GetOption ( options, property, type, values, fallback ) + * + * If the requested property is present and not-undefined, set the result string + * to |ToString(value)|. Otherwise set the result string to nullptr. + */ +static bool GetStringOption(JSContext* cx, HandleObject options, + Handle<PropertyName*> name, + MutableHandle<JSLinearString*> string) { + // Step 1. + RootedValue option(cx); + if (!GetProperty(cx, options, options, name, &option)) { + return false; + } + + // Step 2. + JSLinearString* linear = nullptr; + if (!option.isUndefined()) { + // Steps 2.a-b, 2.d (not applicable). + + // Steps 2.c, 2.e. + JSString* str = ToString(cx, option); + if (!str) { + return false; + } + linear = str->ensureLinear(cx); + if (!linear) { + return false; + } + } + + // Step 3. + string.set(linear); + return true; +} + +/** + * 9.2.10 GetOption ( options, property, type, values, fallback ) + * + * If the requested property is present and not-undefined, set the result string + * to |ToString(ToBoolean(value))|. Otherwise set the result string to nullptr. + */ +static bool GetBooleanOption(JSContext* cx, HandleObject options, + Handle<PropertyName*> name, + MutableHandle<JSLinearString*> string) { + // Step 1. + RootedValue option(cx); + if (!GetProperty(cx, options, options, name, &option)) { + return false; + } + + // Step 2. + JSLinearString* linear = nullptr; + if (!option.isUndefined()) { + // Steps 2.a, 2.c-d (not applicable). + + // Steps 2.c, 2.e. + linear = BooleanToString(cx, ToBoolean(option)); + } + + // Step 3. + string.set(linear); + return true; +} + +/** + * ApplyOptionsToTag ( tag, options ) + */ +static bool ApplyOptionsToTag(JSContext* cx, mozilla::intl::Locale& tag, + HandleObject options) { + // Steps 1-2 (Already performed in caller). + + Rooted<JSLinearString*> option(cx); + + // Step 3. + if (!GetStringOption(cx, options, cx->names().language, &option)) { + return false; + } + + // Step 4. + mozilla::intl::LanguageSubtag language; + if (option && !intl::ParseStandaloneLanguageTag(option, language)) { + if (UniqueChars str = QuoteString(cx, option, '"')) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "language", + str.get()); + } + return false; + } + + // Step 5. + if (!GetStringOption(cx, options, cx->names().script, &option)) { + return false; + } + + // Step 6. + mozilla::intl::ScriptSubtag script; + if (option && !intl::ParseStandaloneScriptTag(option, script)) { + if (UniqueChars str = QuoteString(cx, option, '"')) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "script", + str.get()); + } + return false; + } + + // Step 7. + if (!GetStringOption(cx, options, cx->names().region, &option)) { + return false; + } + + // Step 8. + mozilla::intl::RegionSubtag region; + if (option && !intl::ParseStandaloneRegionTag(option, region)) { + if (UniqueChars str = QuoteString(cx, option, '"')) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "region", + str.get()); + } + return false; + } + + // Step 9 (Already performed in caller). + + // Skip steps 10-13 when no subtags were modified. + if (language.Present() || script.Present() || region.Present()) { + // Step 10. + if (language.Present()) { + tag.SetLanguage(language); + } + + // Step 11. + if (script.Present()) { + tag.SetScript(script); + } + + // Step 12. + if (region.Present()) { + tag.SetRegion(region); + } + + // Step 13. + // Optimized to only canonicalize the base-name subtags. All other + // canonicalization steps will happen later. + auto result = tag.CanonicalizeBaseName(); + if (result.isErr()) { + if (result.unwrapErr() == + mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DUPLICATE_VARIANT_SUBTAG); + } else { + intl::ReportInternalError(cx); + } + return false; + } + } + + return true; +} + +/** + * ApplyUnicodeExtensionToTag( tag, options, relevantExtensionKeys ) + */ +bool js::intl::ApplyUnicodeExtensionToTag( + JSContext* cx, mozilla::intl::Locale& tag, + JS::HandleVector<intl::UnicodeExtensionKeyword> keywords) { + // If no Unicode extensions were present in the options object, we can skip + // everything below and directly return. + if (keywords.length() == 0) { + return true; + } + + Vector<char, 32> newExtension(cx); + if (!newExtension.append('u')) { + return false; + } + + // Check if there's an existing Unicode extension subtag. + + const char* unicodeExtensionEnd = nullptr; + const char* unicodeExtensionKeywords = nullptr; + if (auto unicodeExtension = tag.GetUnicodeExtension()) { + const char* unicodeExtensionBegin = unicodeExtension->data(); + unicodeExtensionEnd = unicodeExtensionBegin + unicodeExtension->size(); + + SepKeywordIterator<char> iter(unicodeExtensionBegin, unicodeExtensionEnd); + + // Find the start of the first keyword. + unicodeExtensionKeywords = iter.next(); + + // Copy any attributes present before the first keyword. + const char* attributesEnd = unicodeExtensionKeywords + ? unicodeExtensionKeywords + : unicodeExtensionEnd; + if (!newExtension.append(unicodeExtensionBegin + 1, attributesEnd)) { + return false; + } + } + + // Append the new keywords before any existing keywords. That way any previous + // keyword with the same key is detected as a duplicate when canonicalizing + // the Unicode extension subtag and gets discarded. + + for (const auto& keyword : keywords) { + UnicodeExtensionKeyword::UnicodeKeySpan key = keyword.key(); + if (!newExtension.append('-')) { + return false; + } + if (!newExtension.append(key.data(), key.size())) { + return false; + } + if (!newExtension.append('-')) { + return false; + } + + JS::AutoCheckCannotGC nogc; + JSLinearString* type = keyword.type(); + if (type->hasLatin1Chars()) { + if (!newExtension.append(type->latin1Chars(nogc), type->length())) { + return false; + } + } else { + if (!newExtension.append(type->twoByteChars(nogc), type->length())) { + return false; + } + } + } + + // Append the remaining keywords from the previous Unicode extension subtag. + if (unicodeExtensionKeywords) { + if (!newExtension.append(unicodeExtensionKeywords, unicodeExtensionEnd)) { + return false; + } + } + + if (auto res = tag.SetUnicodeExtension(newExtension); res.isErr()) { + intl::ReportInternalError(cx, res.unwrapErr()); + return false; + } + + return true; +} + +static JS::Result<JSString*> LanguageTagFromMaybeWrappedLocale(JSContext* cx, + JSObject* obj) { + if (obj->is<LocaleObject>()) { + return obj->as<LocaleObject>().languageTag(); + } + + JSObject* unwrapped = CheckedUnwrapStatic(obj); + if (!unwrapped) { + ReportAccessDenied(cx); + return cx->alreadyReportedError(); + } + + if (!unwrapped->is<LocaleObject>()) { + return nullptr; + } + + RootedString tagStr(cx, unwrapped->as<LocaleObject>().languageTag()); + if (!cx->compartment()->wrap(cx, &tagStr)) { + return cx->alreadyReportedError(); + } + return tagStr.get(); +} + +/** + * Intl.Locale( tag[, options] ) + */ +static bool Locale(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Intl.Locale")) { + return false; + } + + // Steps 2-6 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Locale, &proto)) { + return false; + } + + // Steps 7-9. + HandleValue tagValue = args.get(0); + JSString* tagStr; + if (tagValue.isObject()) { + JS_TRY_VAR_OR_RETURN_FALSE( + cx, tagStr, + LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject())); + if (!tagStr) { + tagStr = ToString(cx, tagValue); + if (!tagStr) { + return false; + } + } + } else if (tagValue.isString()) { + tagStr = tagValue.toString(); + } else { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_LOCALES_ELEMENT); + return false; + } + + Rooted<JSLinearString*> tagLinearStr(cx, tagStr->ensureLinear(cx)); + if (!tagLinearStr) { + return false; + } + + // Steps 10-11. + RootedObject options(cx); + if (args.hasDefined(1)) { + options = ToObject(cx, args[1]); + if (!options) { + return false; + } + } + + // ApplyOptionsToTag, steps 2 and 9. + mozilla::intl::Locale tag; + if (!intl::ParseLocale(cx, tagLinearStr, tag)) { + return false; + } + + if (auto result = tag.CanonicalizeBaseName(); result.isErr()) { + if (result.unwrapErr() == + mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DUPLICATE_VARIANT_SUBTAG); + } else { + intl::ReportInternalError(cx); + } + return false; + } + + if (options) { + // Step 12. + if (!ApplyOptionsToTag(cx, tag, options)) { + return false; + } + + // Step 13. + JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx); + + // Step 14. + Rooted<JSLinearString*> calendar(cx); + if (!GetStringOption(cx, options, cx->names().calendar, &calendar)) { + return false; + } + + // Steps 15-16. + if (calendar) { + bool isValid; + if (!IsValidUnicodeExtensionValue(cx, calendar, &isValid)) { + return false; + } + + if (!isValid) { + if (UniqueChars str = QuoteString(cx, calendar, '"')) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "calendar", + str.get()); + } + return false; + } + + if (!keywords.emplaceBack("ca", calendar)) { + return false; + } + } + + // Step 17. + Rooted<JSLinearString*> collation(cx); + if (!GetStringOption(cx, options, cx->names().collation, &collation)) { + return false; + } + + // Steps 18-19. + if (collation) { + bool isValid; + if (!IsValidUnicodeExtensionValue(cx, collation, &isValid)) { + return false; + } + + if (!isValid) { + if (UniqueChars str = QuoteString(cx, collation, '"')) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "collation", + str.get()); + } + return false; + } + + if (!keywords.emplaceBack("co", collation)) { + return false; + } + } + + // Step 20 (without validation). + Rooted<JSLinearString*> hourCycle(cx); + if (!GetStringOption(cx, options, cx->names().hourCycle, &hourCycle)) { + return false; + } + + // Steps 20-21. + if (hourCycle) { + if (!StringEqualsLiteral(hourCycle, "h11") && + !StringEqualsLiteral(hourCycle, "h12") && + !StringEqualsLiteral(hourCycle, "h23") && + !StringEqualsLiteral(hourCycle, "h24")) { + if (UniqueChars str = QuoteString(cx, hourCycle, '"')) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "hourCycle", + str.get()); + } + return false; + } + + if (!keywords.emplaceBack("hc", hourCycle)) { + return false; + } + } + + // Step 22 (without validation). + Rooted<JSLinearString*> caseFirst(cx); + if (!GetStringOption(cx, options, cx->names().caseFirst, &caseFirst)) { + return false; + } + + // Steps 22-23. + if (caseFirst) { + if (!StringEqualsLiteral(caseFirst, "upper") && + !StringEqualsLiteral(caseFirst, "lower") && + !StringEqualsLiteral(caseFirst, "false")) { + if (UniqueChars str = QuoteString(cx, caseFirst, '"')) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "caseFirst", + str.get()); + } + return false; + } + + if (!keywords.emplaceBack("kf", caseFirst)) { + return false; + } + } + + // Steps 24-25. + Rooted<JSLinearString*> numeric(cx); + if (!GetBooleanOption(cx, options, cx->names().numeric, &numeric)) { + return false; + } + + // Step 26. + if (numeric) { + if (!keywords.emplaceBack("kn", numeric)) { + return false; + } + } + + // Step 27. + Rooted<JSLinearString*> numberingSystem(cx); + if (!GetStringOption(cx, options, cx->names().numberingSystem, + &numberingSystem)) { + return false; + } + + // Steps 28-29. + if (numberingSystem) { + bool isValid; + if (!IsValidUnicodeExtensionValue(cx, numberingSystem, &isValid)) { + return false; + } + if (!isValid) { + if (UniqueChars str = QuoteString(cx, numberingSystem, '"')) { + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, + "numberingSystem", str.get()); + } + return false; + } + + if (!keywords.emplaceBack("nu", numberingSystem)) { + return false; + } + } + + // Step 30. + if (!ApplyUnicodeExtensionToTag(cx, tag, keywords)) { + return false; + } + } + + // ApplyOptionsToTag, steps 9 and 13. + // ApplyUnicodeExtensionToTag, step 9. + if (auto result = tag.CanonicalizeExtensions(); result.isErr()) { + if (result.unwrapErr() == + mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DUPLICATE_VARIANT_SUBTAG); + } else { + intl::ReportInternalError(cx); + } + return false; + } + + // Steps 6, 31-37. + JSObject* obj = CreateLocaleObject(cx, proto, tag); + if (!obj) { + return false; + } + + // Step 38. + args.rval().setObject(*obj); + return true; +} + +using UnicodeKey = const char (&)[UnicodeKeyLength + 1]; + +// Returns the tuple [index, length] of the `type` in the `keyword` in Unicode +// locale extension |extension| that has |key| as its `key`. If `keyword` lacks +// a type, the returned |index| will be where `type` would have been, and +// |length| will be set to zero. +template <typename CharT> +static mozilla::Maybe<IndexAndLength> FindUnicodeExtensionType( + const CharT* extension, size_t length, UnicodeKey key) { + MOZ_ASSERT(extension[0] == 'u'); + MOZ_ASSERT(extension[1] == '-'); + + const CharT* end = extension + length; + + SepKeywordIterator<CharT> iter(extension, end); + + // Search all keywords until a match was found. + const CharT* beginKey; + while (true) { + beginKey = iter.next(); + if (!beginKey) { + return mozilla::Nothing(); + } + + // Add +1 to skip over the separator preceding the keyword. + MOZ_ASSERT(beginKey[0] == '-'); + beginKey++; + + // Exit the loop on the first match. + if (std::equal(beginKey, beginKey + UnicodeKeyLength, key)) { + break; + } + } + + // Skip over the key. + const CharT* beginType = beginKey + UnicodeKeyLength; + + // Find the start of the next keyword. + const CharT* endType = iter.next(); + + // No further keyword present, the current keyword ends the Unicode extension. + if (!endType) { + endType = end; + } + + // If the keyword has a type, skip over the separator preceding the type. + if (beginType != endType) { + MOZ_ASSERT(beginType[0] == '-'); + beginType++; + } + return mozilla::Some(IndexAndLength{size_t(beginType - extension), + size_t(endType - beginType)}); +} + +static inline auto FindUnicodeExtensionType(JSLinearString* unicodeExtension, + UnicodeKey key) { + JS::AutoCheckCannotGC nogc; + return unicodeExtension->hasLatin1Chars() + ? FindUnicodeExtensionType(unicodeExtension->latin1Chars(nogc), + unicodeExtension->length(), key) + : FindUnicodeExtensionType(unicodeExtension->twoByteChars(nogc), + unicodeExtension->length(), key); +} + +// Return the sequence of types for the Unicode extension keyword specified by +// key or undefined when the keyword isn't present. +static bool GetUnicodeExtension(JSContext* cx, LocaleObject* locale, + UnicodeKey key, MutableHandleValue value) { + // Return undefined when no Unicode extension subtag is present. + const Value& unicodeExtensionValue = locale->unicodeExtension(); + if (unicodeExtensionValue.isUndefined()) { + value.setUndefined(); + return true; + } + + JSLinearString* unicodeExtension = + unicodeExtensionValue.toString()->ensureLinear(cx); + if (!unicodeExtension) { + return false; + } + + // Find the type of the requested key in the Unicode extension subtag. + auto result = FindUnicodeExtensionType(unicodeExtension, key); + + // Return undefined if the requested key isn't present in the extension. + if (!result) { + value.setUndefined(); + return true; + } + + size_t index = result->index; + size_t length = result->length; + + // Otherwise return the type value of the found keyword. + JSString* str = NewDependentString(cx, unicodeExtension, index, length); + if (!str) { + return false; + } + value.setString(str); + return true; +} + +struct BaseNamePartsResult { + IndexAndLength language; + mozilla::Maybe<IndexAndLength> script; + mozilla::Maybe<IndexAndLength> region; +}; + +// Returns [language-length, script-index, region-index, region-length]. +template <typename CharT> +static BaseNamePartsResult BaseNameParts(const CharT* baseName, size_t length) { + size_t languageLength; + size_t scriptIndex = 0; + size_t regionIndex = 0; + size_t regionLength = 0; + + // Search the first separator to find the end of the language subtag. + if (const CharT* sep = std::char_traits<CharT>::find(baseName, length, '-')) { + languageLength = sep - baseName; + + // Add +1 to skip over the separator character. + size_t nextSubtag = languageLength + 1; + + // Script subtags are always four characters long, but take care for a four + // character long variant subtag. These start with a digit. + if ((nextSubtag + ScriptLength == length || + (nextSubtag + ScriptLength < length && + baseName[nextSubtag + ScriptLength] == '-')) && + mozilla::IsAsciiAlpha(baseName[nextSubtag])) { + scriptIndex = nextSubtag; + nextSubtag = scriptIndex + ScriptLength + 1; + } + + // Region subtags can be either two or three characters long. + if (nextSubtag < length) { + for (size_t rlen : {AlphaRegionLength, DigitRegionLength}) { + MOZ_ASSERT(nextSubtag + rlen <= length); + if (nextSubtag + rlen == length || baseName[nextSubtag + rlen] == '-') { + regionIndex = nextSubtag; + regionLength = rlen; + break; + } + } + } + } else { + // No separator found, the base-name consists of just a language subtag. + languageLength = length; + } + + // Tell the analysis the |IsStructurallyValid*Tag| functions can't GC. + JS::AutoSuppressGCAnalysis nogc; + + IndexAndLength language{0, languageLength}; + MOZ_ASSERT( + mozilla::intl::IsStructurallyValidLanguageTag(language.spanOf(baseName))); + + mozilla::Maybe<IndexAndLength> script{}; + if (scriptIndex) { + script.emplace(scriptIndex, ScriptLength); + MOZ_ASSERT( + mozilla::intl::IsStructurallyValidScriptTag(script->spanOf(baseName))); + } + + mozilla::Maybe<IndexAndLength> region{}; + if (regionIndex) { + region.emplace(regionIndex, regionLength); + MOZ_ASSERT( + mozilla::intl::IsStructurallyValidRegionTag(region->spanOf(baseName))); + } + + return {language, script, region}; +} + +static inline auto BaseNameParts(JSLinearString* baseName) { + JS::AutoCheckCannotGC nogc; + return baseName->hasLatin1Chars() + ? BaseNameParts(baseName->latin1Chars(nogc), baseName->length()) + : BaseNameParts(baseName->twoByteChars(nogc), baseName->length()); +} + +// Intl.Locale.prototype.maximize () +static bool Locale_maximize(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as<LocaleObject>(); + Rooted<JSLinearString*> tagStr(cx, locale->languageTag()->ensureLinear(cx)); + if (!tagStr) { + return false; + } + + mozilla::intl::Locale tag; + if (!intl::ParseLocale(cx, tagStr, tag)) { + return false; + } + + if (auto result = tag.AddLikelySubtags(); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + // Step 4. + auto* result = CreateLocaleObject(cx, nullptr, tag); + if (!result) { + return false; + } + args.rval().setObject(*result); + return true; +} + +// Intl.Locale.prototype.maximize () +static bool Locale_maximize(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsLocale, Locale_maximize>(cx, args); +} + +// Intl.Locale.prototype.minimize () +static bool Locale_minimize(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as<LocaleObject>(); + Rooted<JSLinearString*> tagStr(cx, locale->languageTag()->ensureLinear(cx)); + if (!tagStr) { + return false; + } + + mozilla::intl::Locale tag; + if (!intl::ParseLocale(cx, tagStr, tag)) { + return false; + } + + if (auto result = tag.RemoveLikelySubtags(); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + // Step 4. + auto* result = CreateLocaleObject(cx, nullptr, tag); + if (!result) { + return false; + } + args.rval().setObject(*result); + return true; +} + +// Intl.Locale.prototype.minimize () +static bool Locale_minimize(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsLocale, Locale_minimize>(cx, args); +} + +// Intl.Locale.prototype.toString () +static bool Locale_toString(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as<LocaleObject>(); + args.rval().setString(locale->languageTag()); + return true; +} + +// Intl.Locale.prototype.toString () +static bool Locale_toString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsLocale, Locale_toString>(cx, args); +} + +// get Intl.Locale.prototype.baseName +static bool Locale_baseName(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Steps 3-4. + auto* locale = &args.thisv().toObject().as<LocaleObject>(); + args.rval().setString(locale->baseName()); + return true; +} + +// get Intl.Locale.prototype.baseName +static bool Locale_baseName(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsLocale, Locale_baseName>(cx, args); +} + +// get Intl.Locale.prototype.calendar +static bool Locale_calendar(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as<LocaleObject>(); + return GetUnicodeExtension(cx, locale, "ca", args.rval()); +} + +// get Intl.Locale.prototype.calendar +static bool Locale_calendar(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsLocale, Locale_calendar>(cx, args); +} + +// get Intl.Locale.prototype.caseFirst +static bool Locale_caseFirst(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as<LocaleObject>(); + return GetUnicodeExtension(cx, locale, "kf", args.rval()); +} + +// get Intl.Locale.prototype.caseFirst +static bool Locale_caseFirst(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsLocale, Locale_caseFirst>(cx, args); +} + +// get Intl.Locale.prototype.collation +static bool Locale_collation(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as<LocaleObject>(); + return GetUnicodeExtension(cx, locale, "co", args.rval()); +} + +// get Intl.Locale.prototype.collation +static bool Locale_collation(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsLocale, Locale_collation>(cx, args); +} + +// get Intl.Locale.prototype.hourCycle +static bool Locale_hourCycle(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as<LocaleObject>(); + return GetUnicodeExtension(cx, locale, "hc", args.rval()); +} + +// get Intl.Locale.prototype.hourCycle +static bool Locale_hourCycle(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsLocale, Locale_hourCycle>(cx, args); +} + +// get Intl.Locale.prototype.numeric +static bool Locale_numeric(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as<LocaleObject>(); + RootedValue value(cx); + if (!GetUnicodeExtension(cx, locale, "kn", &value)) { + return false; + } + + // Compare against the empty string per Intl.Locale, step 36.a. The Unicode + // extension is already canonicalized, so we don't need to compare against + // "true" at this point. + MOZ_ASSERT(value.isUndefined() || value.isString()); + MOZ_ASSERT_IF(value.isString(), + !StringEqualsLiteral(&value.toString()->asLinear(), "true")); + + args.rval().setBoolean(value.isString() && value.toString()->empty()); + return true; +} + +// get Intl.Locale.prototype.numeric +static bool Locale_numeric(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsLocale, Locale_numeric>(cx, args); +} + +// get Intl.Locale.prototype.numberingSystem +static bool Intl_Locale_numberingSystem(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as<LocaleObject>(); + return GetUnicodeExtension(cx, locale, "nu", args.rval()); +} + +// get Intl.Locale.prototype.numberingSystem +static bool Locale_numberingSystem(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsLocale, Intl_Locale_numberingSystem>(cx, args); +} + +// get Intl.Locale.prototype.language +static bool Locale_language(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as<LocaleObject>(); + JSLinearString* baseName = locale->baseName()->ensureLinear(cx); + if (!baseName) { + return false; + } + + // Step 4 (Unnecessary assertion). + + auto language = BaseNameParts(baseName).language; + + size_t index = language.index; + size_t length = language.length; + + // Step 5. + JSString* str = NewDependentString(cx, baseName, index, length); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +// get Intl.Locale.prototype.language +static bool Locale_language(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsLocale, Locale_language>(cx, args); +} + +// get Intl.Locale.prototype.script +static bool Locale_script(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as<LocaleObject>(); + JSLinearString* baseName = locale->baseName()->ensureLinear(cx); + if (!baseName) { + return false; + } + + // Step 4 (Unnecessary assertion). + + auto script = BaseNameParts(baseName).script; + + // Step 5. + if (!script) { + args.rval().setUndefined(); + return true; + } + + size_t index = script->index; + size_t length = script->length; + + // Step 6. + JSString* str = NewDependentString(cx, baseName, index, length); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +// get Intl.Locale.prototype.script +static bool Locale_script(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsLocale, Locale_script>(cx, args); +} + +// get Intl.Locale.prototype.region +static bool Locale_region(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as<LocaleObject>(); + JSLinearString* baseName = locale->baseName()->ensureLinear(cx); + if (!baseName) { + return false; + } + + // Step 4 (Unnecessary assertion). + + auto region = BaseNameParts(baseName).region; + + // Step 5. + if (!region) { + args.rval().setUndefined(); + return true; + } + + size_t index = region->index; + size_t length = region->length; + + // Step 6. + JSString* str = NewDependentString(cx, baseName, index, length); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +// get Intl.Locale.prototype.region +static bool Locale_region(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsLocale, Locale_region>(cx, args); +} + +static bool Locale_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().Locale); + return true; +} + +static const JSFunctionSpec locale_methods[] = { + JS_FN("maximize", Locale_maximize, 0, 0), + JS_FN("minimize", Locale_minimize, 0, 0), + JS_FN(js_toString_str, Locale_toString, 0, 0), + JS_FN(js_toSource_str, Locale_toSource, 0, 0), JS_FS_END}; + +static const JSPropertySpec locale_properties[] = { + JS_PSG("baseName", Locale_baseName, 0), + JS_PSG("calendar", Locale_calendar, 0), + JS_PSG("caseFirst", Locale_caseFirst, 0), + JS_PSG("collation", Locale_collation, 0), + JS_PSG("hourCycle", Locale_hourCycle, 0), + JS_PSG("numeric", Locale_numeric, 0), + JS_PSG("numberingSystem", Locale_numberingSystem, 0), + JS_PSG("language", Locale_language, 0), + JS_PSG("script", Locale_script, 0), + JS_PSG("region", Locale_region, 0), + JS_STRING_SYM_PS(toStringTag, "Intl.Locale", JSPROP_READONLY), + JS_PS_END}; + +const ClassSpec LocaleObject::classSpec_ = { + GenericCreateConstructor<Locale, 1, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<LocaleObject>, + nullptr, + nullptr, + locale_methods, + locale_properties, + nullptr, + ClassSpec::DontDefineConstructor}; + +bool js::intl_ValidateAndCanonicalizeLanguageTag(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + + HandleValue tagValue = args[0]; + bool applyToString = args[1].toBoolean(); + + if (tagValue.isObject()) { + JSString* tagStr; + JS_TRY_VAR_OR_RETURN_FALSE( + cx, tagStr, + LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject())); + if (tagStr) { + args.rval().setString(tagStr); + return true; + } + } + + if (!applyToString && !tagValue.isString()) { + args.rval().setNull(); + return true; + } + + JSString* tagStr = ToString(cx, tagValue); + if (!tagStr) { + return false; + } + + Rooted<JSLinearString*> tagLinearStr(cx, tagStr->ensureLinear(cx)); + if (!tagLinearStr) { + return false; + } + + // Handle the common case (a standalone language) first. + // Only the following Unicode BCP 47 locale identifier subset is accepted: + // unicode_locale_id = unicode_language_id + // unicode_language_id = unicode_language_subtag + // unicode_language_subtag = alpha{2,3} + JSString* language; + JS_TRY_VAR_OR_RETURN_FALSE( + cx, language, intl::ParseStandaloneISO639LanguageTag(cx, tagLinearStr)); + if (language) { + args.rval().setString(language); + return true; + } + + mozilla::intl::Locale tag; + if (!intl::ParseLocale(cx, tagLinearStr, tag)) { + return false; + } + + auto result = tag.Canonicalize(); + if (result.isErr()) { + if (result.unwrapErr() == + mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DUPLICATE_VARIANT_SUBTAG); + } else { + intl::ReportInternalError(cx); + } + return false; + } + + intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + if (auto result = tag.ToString(buffer); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + JSString* resultStr = buffer.toAsciiString(cx); + if (!resultStr) { + return false; + } + + args.rval().setString(resultStr); + return true; +} + +bool js::intl_TryValidateAndCanonicalizeLanguageTag(JSContext* cx, + unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + Rooted<JSLinearString*> linear(cx, args[0].toString()->ensureLinear(cx)); + if (!linear) { + return false; + } + + mozilla::intl::Locale tag; + { + if (!StringIsAscii(linear)) { + // The caller handles invalid inputs. + args.rval().setNull(); + return true; + } + + intl::StringAsciiChars chars(linear); + if (!chars.init(cx)) { + return false; + } + + if (mozilla::intl::LocaleParser::TryParse(chars, tag).isErr()) { + // The caller handles invalid inputs. + args.rval().setNull(); + return true; + } + } + + auto result = tag.Canonicalize(); + if (result.isErr()) { + if (result.unwrapErr() == + mozilla::intl::Locale::CanonicalizationError::DuplicateVariant) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DUPLICATE_VARIANT_SUBTAG); + } else { + intl::ReportInternalError(cx); + } + return false; + } + + intl::FormatBuffer<char, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + if (auto result = tag.ToString(buffer); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + JSString* resultStr = buffer.toAsciiString(cx); + if (!resultStr) { + return false; + } + args.rval().setString(resultStr); + return true; +} + +bool js::intl_ValidateAndCanonicalizeUnicodeExtensionType(JSContext* cx, + unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + + HandleValue typeArg = args[0]; + MOZ_ASSERT(typeArg.isString(), "type must be a string"); + + HandleValue optionArg = args[1]; + MOZ_ASSERT(optionArg.isString(), "option name must be a string"); + + HandleValue keyArg = args[2]; + MOZ_ASSERT(keyArg.isString(), "key must be a string"); + + Rooted<JSLinearString*> unicodeType(cx, typeArg.toString()->ensureLinear(cx)); + if (!unicodeType) { + return false; + } + + bool isValid; + if (!IsValidUnicodeExtensionValue(cx, unicodeType, &isValid)) { + return false; + } + if (!isValid) { + UniqueChars optionChars = EncodeAscii(cx, optionArg.toString()); + if (!optionChars) { + return false; + } + + UniqueChars unicodeTypeChars = QuoteString(cx, unicodeType, '"'); + if (!unicodeTypeChars) { + return false; + } + + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, optionChars.get(), + unicodeTypeChars.get()); + return false; + } + + char unicodeKey[UnicodeKeyLength]; + { + JSLinearString* str = keyArg.toString()->ensureLinear(cx); + if (!str) { + return false; + } + MOZ_ASSERT(str->length() == UnicodeKeyLength); + + for (size_t i = 0; i < UnicodeKeyLength; i++) { + char16_t ch = str->latin1OrTwoByteChar(i); + MOZ_ASSERT(mozilla::IsAscii(ch)); + unicodeKey[i] = char(ch); + } + } + + UniqueChars unicodeTypeChars = EncodeAscii(cx, unicodeType); + if (!unicodeTypeChars) { + return false; + } + + size_t unicodeTypeLength = unicodeType->length(); + MOZ_ASSERT(strlen(unicodeTypeChars.get()) == unicodeTypeLength); + + // Convert into canonical case before searching for replacements. + mozilla::intl::AsciiToLowerCase(unicodeTypeChars.get(), unicodeTypeLength, + unicodeTypeChars.get()); + + auto key = mozilla::Span(unicodeKey, UnicodeKeyLength); + auto type = mozilla::Span(unicodeTypeChars.get(), unicodeTypeLength); + + // Search if there's a replacement for the current Unicode keyword. + JSString* result; + if (const char* replacement = + mozilla::intl::Locale::ReplaceUnicodeExtensionType(key, type)) { + result = NewStringCopyZ<CanGC>(cx, replacement); + } else { + result = StringToLowerCase(cx, unicodeType); + } + if (!result) { + return false; + } + + args.rval().setString(result); + return true; +} diff --git a/js/src/builtin/intl/Locale.h b/js/src/builtin/intl/Locale.h new file mode 100644 index 0000000000..93b618528a --- /dev/null +++ b/js/src/builtin/intl/Locale.h @@ -0,0 +1,61 @@ +/* -*- 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 builtin_intl_Locale_h +#define builtin_intl_Locale_h + +#include <stdint.h> + +#include "js/Class.h" +#include "vm/NativeObject.h" + +namespace js { + +class LocaleObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t LANGUAGE_TAG_SLOT = 0; + static constexpr uint32_t BASENAME_SLOT = 1; + static constexpr uint32_t UNICODE_EXTENSION_SLOT = 2; + static constexpr uint32_t SLOT_COUNT = 3; + + /** + * Returns the complete language tag, including any extensions and privateuse + * subtags. + */ + JSString* languageTag() const { + return getFixedSlot(LANGUAGE_TAG_SLOT).toString(); + } + + /** + * Returns the basename subtags, i.e. excluding any extensions and privateuse + * subtags. + */ + JSString* baseName() const { return getFixedSlot(BASENAME_SLOT).toString(); } + + const Value& unicodeExtension() const { + return getFixedSlot(UNICODE_EXTENSION_SLOT); + } + + private: + static const ClassSpec classSpec_; +}; + +[[nodiscard]] extern bool intl_ValidateAndCanonicalizeLanguageTag(JSContext* cx, + unsigned argc, + Value* vp); + +[[nodiscard]] extern bool intl_TryValidateAndCanonicalizeLanguageTag( + JSContext* cx, unsigned argc, Value* vp); + +[[nodiscard]] extern bool intl_ValidateAndCanonicalizeUnicodeExtensionType( + JSContext* cx, unsigned argc, Value* vp); + +} // namespace js + +#endif /* builtin_intl_Locale_h */ diff --git a/js/src/builtin/intl/NumberFormat.cpp b/js/src/builtin/intl/NumberFormat.cpp new file mode 100644 index 0000000000..df7d6b6aca --- /dev/null +++ b/js/src/builtin/intl/NumberFormat.cpp @@ -0,0 +1,1374 @@ +/* -*- 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/. */ + +/* Intl.NumberFormat implementation. */ + +#include "builtin/intl/NumberFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Casting.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/intl/Locale.h" +#include "mozilla/intl/MeasureUnit.h" +#include "mozilla/intl/MeasureUnitGenerated.h" +#include "mozilla/intl/NumberFormat.h" +#include "mozilla/intl/NumberingSystem.h" +#include "mozilla/intl/NumberRangeFormat.h" +#include "mozilla/Span.h" +#include "mozilla/TextUtils.h" +#include "mozilla/UniquePtr.h" + +#include <algorithm> +#include <stddef.h> +#include <stdint.h> +#include <string> +#include <string_view> +#include <type_traits> + +#include "builtin/Array.h" +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/DecimalNumber.h" +#include "builtin/intl/FormatBuffer.h" +#include "builtin/intl/LanguageTag.h" +#include "builtin/intl/RelativeTimeFormat.h" +#include "gc/GCContext.h" +#include "js/CharacterEncoding.h" +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "util/Text.h" +#include "vm/BigIntType.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/StringType.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/GeckoProfiler-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::AssertedCast; + +using js::intl::DateTimeFormatOptions; +using js::intl::FieldType; + +const JSClassOps NumberFormatObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + NumberFormatObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass NumberFormatObject::class_ = { + "Intl.NumberFormat", + JSCLASS_HAS_RESERVED_SLOTS(NumberFormatObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_NumberFormat) | + JSCLASS_FOREGROUND_FINALIZE, + &NumberFormatObject::classOps_, &NumberFormatObject::classSpec_}; + +const JSClass& NumberFormatObject::protoClass_ = PlainObject::class_; + +static bool numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().NumberFormat); + return true; +} + +static const JSFunctionSpec numberFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", + "Intl_NumberFormat_supportedLocalesOf", 1, 0), + JS_FS_END, +}; + +static const JSFunctionSpec numberFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_NumberFormat_resolvedOptions", 0, + 0), + JS_SELF_HOSTED_FN("formatToParts", "Intl_NumberFormat_formatToParts", 1, 0), +#ifdef NIGHTLY_BUILD + JS_SELF_HOSTED_FN("formatRange", "Intl_NumberFormat_formatRange", 2, 0), + JS_SELF_HOSTED_FN("formatRangeToParts", + "Intl_NumberFormat_formatRangeToParts", 2, 0), +#endif + JS_FN(js_toSource_str, numberFormat_toSource, 0, 0), + JS_FS_END, +}; + +static const JSPropertySpec numberFormat_properties[] = { + JS_SELF_HOSTED_GET("format", "$Intl_NumberFormat_format_get", 0), + JS_STRING_SYM_PS(toStringTag, "Intl.NumberFormat", JSPROP_READONLY), + JS_PS_END, +}; + +static bool NumberFormat(JSContext* cx, unsigned argc, Value* vp); + +const ClassSpec NumberFormatObject::classSpec_ = { + GenericCreateConstructor<NumberFormat, 0, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<NumberFormatObject>, + numberFormat_static_methods, + nullptr, + numberFormat_methods, + numberFormat_properties, + nullptr, + ClassSpec::DontDefineConstructor}; + +/** + * 11.2.1 Intl.NumberFormat([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool NumberFormat(JSContext* cx, const CallArgs& args, bool construct) { + AutoJSConstructorProfilerEntry pseudoFrame(cx, "Intl.NumberFormat"); + + // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). + + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_NumberFormat, + &proto)) { + return false; + } + + Rooted<NumberFormatObject*> numberFormat(cx); + numberFormat = NewObjectWithClassProto<NumberFormatObject>(cx, proto); + if (!numberFormat) { + return false; + } + + RootedValue thisValue(cx, + construct ? ObjectValue(*numberFormat) : args.thisv()); + HandleValue locales = args.get(0); + HandleValue options = args.get(1); + + // Step 3. + return intl::LegacyInitializeObject( + cx, numberFormat, cx->names().InitializeNumberFormat, thisValue, locales, + options, DateTimeFormatOptions::Standard, args.rval()); +} + +static bool NumberFormat(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return NumberFormat(cx, args, args.isConstructing()); +} + +bool js::intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + // intl_NumberFormat is an intrinsic for self-hosted JavaScript, so it + // cannot be used with "new", but it still has to be treated as a + // constructor. + return NumberFormat(cx, args, true); +} + +void js::NumberFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + + auto* numberFormat = &obj->as<NumberFormatObject>(); + mozilla::intl::NumberFormat* nf = numberFormat->getNumberFormatter(); + mozilla::intl::NumberRangeFormat* nrf = + numberFormat->getNumberRangeFormatter(); + + if (nf) { + intl::RemoveICUCellMemory(gcx, obj, NumberFormatObject::EstimatedMemoryUse); + // This was allocated using `new` in mozilla::intl::NumberFormat, so we + // delete here. + delete nf; + } + + if (nrf) { + intl::RemoveICUCellMemory(gcx, obj, EstimatedRangeFormatterMemoryUse); + // This was allocated using `new` in mozilla::intl::NumberRangeFormat, so we + // delete here. + delete nrf; + } +} + +bool js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + UniqueChars locale = intl::EncodeLocale(cx, args[0].toString()); + if (!locale) { + return false; + } + + auto numberingSystem = + mozilla::intl::NumberingSystem::TryCreate(locale.get()); + if (numberingSystem.isErr()) { + intl::ReportInternalError(cx, numberingSystem.unwrapErr()); + return false; + } + + auto name = numberingSystem.inspect()->GetName(); + if (name.isErr()) { + intl::ReportInternalError(cx, name.unwrapErr()); + return false; + } + + JSString* jsname = NewStringCopy<CanGC>(cx, name.unwrap()); + if (!jsname) { + return false; + } + + args.rval().setString(jsname); + return true; +} + +#if DEBUG || MOZ_SYSTEM_ICU +bool js::intl_availableMeasurementUnits(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + RootedObject measurementUnits(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!measurementUnits) { + return false; + } + + auto units = mozilla::intl::MeasureUnit::GetAvailable(); + if (units.isErr()) { + intl::ReportInternalError(cx, units.unwrapErr()); + return false; + } + + Rooted<JSAtom*> unitAtom(cx); + for (auto unit : units.unwrap()) { + if (unit.isErr()) { + intl::ReportInternalError(cx); + return false; + } + auto unitIdentifier = unit.unwrap(); + + unitAtom = Atomize(cx, unitIdentifier.data(), unitIdentifier.size()); + if (!unitAtom) { + return false; + } + + if (!DefineDataProperty(cx, measurementUnits, unitAtom->asPropertyName(), + TrueHandleValue)) { + return false; + } + } + + args.rval().setObject(*measurementUnits); + return true; +} +#endif + +static constexpr size_t MaxUnitLength() { + size_t length = 0; + for (const auto& unit : mozilla::intl::simpleMeasureUnits) { + length = std::max(length, std::char_traits<char>::length(unit.name)); + } + return length * 2 + std::char_traits<char>::length("-per-"); +} + +static UniqueChars NumberFormatLocale(JSContext* cx, HandleObject internals) { + RootedValue value(cx); + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) { + return nullptr; + } + + // ICU expects numberingSystem as a Unicode locale extensions on locale. + + mozilla::intl::Locale tag; + { + Rooted<JSLinearString*> locale(cx, value.toString()->ensureLinear(cx)); + if (!locale) { + return nullptr; + } + + if (!intl::ParseLocale(cx, locale, tag)) { + return nullptr; + } + } + + JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx); + + if (!GetProperty(cx, internals, internals, cx->names().numberingSystem, + &value)) { + return nullptr; + } + + { + JSLinearString* numberingSystem = value.toString()->ensureLinear(cx); + if (!numberingSystem) { + return nullptr; + } + + if (!keywords.emplaceBack("nu", numberingSystem)) { + return nullptr; + } + } + + // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of + // the Unicode extension subtag. We're then relying on ICU to follow RFC + // 6067, which states that any trailing keywords using the same key + // should be ignored. + if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) { + return nullptr; + } + + intl::FormatBuffer<char> buffer(cx); + if (auto result = tag.ToString(buffer); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + return buffer.extractStringZ(); +} + +struct NumberFormatOptions : public mozilla::intl::NumberRangeFormatOptions { + static_assert(std::is_base_of_v<mozilla::intl::NumberFormatOptions, + mozilla::intl::NumberRangeFormatOptions>); + + char currencyChars[3] = {}; + char unitChars[MaxUnitLength()] = {}; +}; + +static bool FillNumberFormatOptions(JSContext* cx, HandleObject internals, + NumberFormatOptions& options) { + RootedValue value(cx); + if (!GetProperty(cx, internals, internals, cx->names().style, &value)) { + return false; + } + + bool accountingSign = false; + { + JSLinearString* style = value.toString()->ensureLinear(cx); + if (!style) { + return false; + } + + if (StringEqualsLiteral(style, "currency")) { + if (!GetProperty(cx, internals, internals, cx->names().currency, + &value)) { + return false; + } + JSLinearString* currency = value.toString()->ensureLinear(cx); + if (!currency) { + return false; + } + + MOZ_RELEASE_ASSERT( + currency->length() == 3, + "IsWellFormedCurrencyCode permits only length-3 strings"); + MOZ_ASSERT(StringIsAscii(currency), + "IsWellFormedCurrencyCode permits only ASCII strings"); + CopyChars(reinterpret_cast<Latin1Char*>(options.currencyChars), + *currency); + + if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay, + &value)) { + return false; + } + JSLinearString* currencyDisplay = value.toString()->ensureLinear(cx); + if (!currencyDisplay) { + return false; + } + + using CurrencyDisplay = + mozilla::intl::NumberFormatOptions::CurrencyDisplay; + + CurrencyDisplay display; + if (StringEqualsLiteral(currencyDisplay, "code")) { + display = CurrencyDisplay::Code; + } else if (StringEqualsLiteral(currencyDisplay, "symbol")) { + display = CurrencyDisplay::Symbol; + } else if (StringEqualsLiteral(currencyDisplay, "narrowSymbol")) { + display = CurrencyDisplay::NarrowSymbol; + } else { + MOZ_ASSERT(StringEqualsLiteral(currencyDisplay, "name")); + display = CurrencyDisplay::Name; + } + + if (!GetProperty(cx, internals, internals, cx->names().currencySign, + &value)) { + return false; + } + JSLinearString* currencySign = value.toString()->ensureLinear(cx); + if (!currencySign) { + return false; + } + + if (StringEqualsLiteral(currencySign, "accounting")) { + accountingSign = true; + } else { + MOZ_ASSERT(StringEqualsLiteral(currencySign, "standard")); + } + + options.mCurrency = mozilla::Some( + std::make_pair(std::string_view(options.currencyChars, 3), display)); + } else if (StringEqualsLiteral(style, "percent")) { + options.mPercent = true; + } else if (StringEqualsLiteral(style, "unit")) { + if (!GetProperty(cx, internals, internals, cx->names().unit, &value)) { + return false; + } + JSLinearString* unit = value.toString()->ensureLinear(cx); + if (!unit) { + return false; + } + + size_t unit_str_length = unit->length(); + + MOZ_ASSERT(StringIsAscii(unit)); + MOZ_RELEASE_ASSERT(unit_str_length <= MaxUnitLength()); + CopyChars(reinterpret_cast<Latin1Char*>(options.unitChars), *unit); + + if (!GetProperty(cx, internals, internals, cx->names().unitDisplay, + &value)) { + return false; + } + JSLinearString* unitDisplay = value.toString()->ensureLinear(cx); + if (!unitDisplay) { + return false; + } + + using UnitDisplay = mozilla::intl::NumberFormatOptions::UnitDisplay; + + UnitDisplay display; + if (StringEqualsLiteral(unitDisplay, "short")) { + display = UnitDisplay::Short; + } else if (StringEqualsLiteral(unitDisplay, "narrow")) { + display = UnitDisplay::Narrow; + } else { + MOZ_ASSERT(StringEqualsLiteral(unitDisplay, "long")); + display = UnitDisplay::Long; + } + + options.mUnit = mozilla::Some(std::make_pair( + std::string_view(options.unitChars, unit_str_length), display)); + } else { + MOZ_ASSERT(StringEqualsLiteral(style, "decimal")); + } + } + + bool hasMinimumSignificantDigits; + if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits, + &hasMinimumSignificantDigits)) { + return false; + } + + if (hasMinimumSignificantDigits) { + if (!GetProperty(cx, internals, internals, + cx->names().minimumSignificantDigits, &value)) { + return false; + } + uint32_t minimumSignificantDigits = AssertedCast<uint32_t>(value.toInt32()); + + if (!GetProperty(cx, internals, internals, + cx->names().maximumSignificantDigits, &value)) { + return false; + } + uint32_t maximumSignificantDigits = AssertedCast<uint32_t>(value.toInt32()); + + options.mSignificantDigits = mozilla::Some( + std::make_pair(minimumSignificantDigits, maximumSignificantDigits)); + } + + bool hasMinimumFractionDigits; + if (!HasProperty(cx, internals, cx->names().minimumFractionDigits, + &hasMinimumFractionDigits)) { + return false; + } + + if (hasMinimumFractionDigits) { + if (!GetProperty(cx, internals, internals, + cx->names().minimumFractionDigits, &value)) { + return false; + } + uint32_t minimumFractionDigits = AssertedCast<uint32_t>(value.toInt32()); + + if (!GetProperty(cx, internals, internals, + cx->names().maximumFractionDigits, &value)) { + return false; + } + uint32_t maximumFractionDigits = AssertedCast<uint32_t>(value.toInt32()); + + options.mFractionDigits = mozilla::Some( + std::make_pair(minimumFractionDigits, maximumFractionDigits)); + } + + if (!GetProperty(cx, internals, internals, cx->names().roundingPriority, + &value)) { + return false; + } + + { + JSLinearString* roundingPriority = value.toString()->ensureLinear(cx); + if (!roundingPriority) { + return false; + } + + using RoundingPriority = + mozilla::intl::NumberFormatOptions::RoundingPriority; + + RoundingPriority priority; + if (StringEqualsLiteral(roundingPriority, "auto")) { + priority = RoundingPriority::Auto; + } else if (StringEqualsLiteral(roundingPriority, "morePrecision")) { + priority = RoundingPriority::MorePrecision; + } else { + MOZ_ASSERT(StringEqualsLiteral(roundingPriority, "lessPrecision")); + priority = RoundingPriority::LessPrecision; + } + + options.mRoundingPriority = priority; + } + + if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, + &value)) { + return false; + } + options.mMinIntegerDigits = + mozilla::Some(AssertedCast<uint32_t>(value.toInt32())); + + if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) { + return false; + } + + if (value.isString()) { + JSLinearString* useGrouping = value.toString()->ensureLinear(cx); + if (!useGrouping) { + return false; + } + + using Grouping = mozilla::intl::NumberFormatOptions::Grouping; + + Grouping grouping; + if (StringEqualsLiteral(useGrouping, "auto")) { + grouping = Grouping::Auto; + } else if (StringEqualsLiteral(useGrouping, "always")) { + grouping = Grouping::Always; + } else { + MOZ_ASSERT(StringEqualsLiteral(useGrouping, "min2")); + grouping = Grouping::Min2; + } + + options.mGrouping = grouping; + } else { + MOZ_ASSERT(value.isBoolean()); +#ifdef NIGHTLY_BUILD + // The caller passes the string "always" instead of |true| when the + // NumberFormat V3 spec is being used. + MOZ_ASSERT(value.toBoolean() == false); +#endif + + using Grouping = mozilla::intl::NumberFormatOptions::Grouping; + + Grouping grouping; + if (value.toBoolean()) { + grouping = Grouping::Auto; + } else { + grouping = Grouping::Never; + } + + options.mGrouping = grouping; + } + + if (!GetProperty(cx, internals, internals, cx->names().notation, &value)) { + return false; + } + + { + JSLinearString* notation = value.toString()->ensureLinear(cx); + if (!notation) { + return false; + } + + using Notation = mozilla::intl::NumberFormatOptions::Notation; + + Notation style; + if (StringEqualsLiteral(notation, "standard")) { + style = Notation::Standard; + } else if (StringEqualsLiteral(notation, "scientific")) { + style = Notation::Scientific; + } else if (StringEqualsLiteral(notation, "engineering")) { + style = Notation::Engineering; + } else { + MOZ_ASSERT(StringEqualsLiteral(notation, "compact")); + + if (!GetProperty(cx, internals, internals, cx->names().compactDisplay, + &value)) { + return false; + } + + JSLinearString* compactDisplay = value.toString()->ensureLinear(cx); + if (!compactDisplay) { + return false; + } + + if (StringEqualsLiteral(compactDisplay, "short")) { + style = Notation::CompactShort; + } else { + MOZ_ASSERT(StringEqualsLiteral(compactDisplay, "long")); + style = Notation::CompactLong; + } + } + + options.mNotation = style; + } + + if (!GetProperty(cx, internals, internals, cx->names().signDisplay, &value)) { + return false; + } + + { + JSLinearString* signDisplay = value.toString()->ensureLinear(cx); + if (!signDisplay) { + return false; + } + + using SignDisplay = mozilla::intl::NumberFormatOptions::SignDisplay; + + SignDisplay display; + if (StringEqualsLiteral(signDisplay, "auto")) { + if (accountingSign) { + display = SignDisplay::Accounting; + } else { + display = SignDisplay::Auto; + } + } else if (StringEqualsLiteral(signDisplay, "never")) { + display = SignDisplay::Never; + } else if (StringEqualsLiteral(signDisplay, "always")) { + if (accountingSign) { + display = SignDisplay::AccountingAlways; + } else { + display = SignDisplay::Always; + } + } else if (StringEqualsLiteral(signDisplay, "exceptZero")) { + if (accountingSign) { + display = SignDisplay::AccountingExceptZero; + } else { + display = SignDisplay::ExceptZero; + } + } else { + MOZ_ASSERT(StringEqualsLiteral(signDisplay, "negative")); + if (accountingSign) { + display = SignDisplay::AccountingNegative; + } else { + display = SignDisplay::Negative; + } + } + + options.mSignDisplay = display; + } + + if (!GetProperty(cx, internals, internals, cx->names().roundingIncrement, + &value)) { + return false; + } + options.mRoundingIncrement = AssertedCast<uint32_t>(value.toInt32()); + + if (!GetProperty(cx, internals, internals, cx->names().roundingMode, + &value)) { + return false; + } + + { + JSLinearString* roundingMode = value.toString()->ensureLinear(cx); + if (!roundingMode) { + return false; + } + + using RoundingMode = mozilla::intl::NumberFormatOptions::RoundingMode; + + RoundingMode rounding; + if (StringEqualsLiteral(roundingMode, "halfExpand")) { + // "halfExpand" is the default mode, so we handle it first. + rounding = RoundingMode::HalfExpand; + } else if (StringEqualsLiteral(roundingMode, "ceil")) { + rounding = RoundingMode::Ceil; + } else if (StringEqualsLiteral(roundingMode, "floor")) { + rounding = RoundingMode::Floor; + } else if (StringEqualsLiteral(roundingMode, "expand")) { + rounding = RoundingMode::Expand; + } else if (StringEqualsLiteral(roundingMode, "trunc")) { + rounding = RoundingMode::Trunc; + } else if (StringEqualsLiteral(roundingMode, "halfCeil")) { + rounding = RoundingMode::HalfCeil; + } else if (StringEqualsLiteral(roundingMode, "halfFloor")) { + rounding = RoundingMode::HalfFloor; + } else if (StringEqualsLiteral(roundingMode, "halfTrunc")) { + rounding = RoundingMode::HalfTrunc; + } else { + MOZ_ASSERT(StringEqualsLiteral(roundingMode, "halfEven")); + rounding = RoundingMode::HalfEven; + } + + options.mRoundingMode = rounding; + } + + if (!GetProperty(cx, internals, internals, cx->names().trailingZeroDisplay, + &value)) { + return false; + } + + { + JSLinearString* trailingZeroDisplay = value.toString()->ensureLinear(cx); + if (!trailingZeroDisplay) { + return false; + } + + if (StringEqualsLiteral(trailingZeroDisplay, "auto")) { + options.mStripTrailingZero = false; + } else { + MOZ_ASSERT(StringEqualsLiteral(trailingZeroDisplay, "stripIfInteger")); + options.mStripTrailingZero = true; + } + } + + return true; +} + +/** + * Returns a new mozilla::intl::Number[Range]Format with the locale and number + * formatting options of the given NumberFormat, or a nullptr if + * initialization failed. + */ +template <class Formatter> +static Formatter* NewNumberFormat(JSContext* cx, + Handle<NumberFormatObject*> numberFormat) { + RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat)); + if (!internals) { + return nullptr; + } + + UniqueChars locale = NumberFormatLocale(cx, internals); + if (!locale) { + return nullptr; + } + + NumberFormatOptions options; + if (!FillNumberFormatOptions(cx, internals, options)) { + return nullptr; + } + + options.mRangeCollapse = NumberFormatOptions::RangeCollapse::Auto; + options.mRangeIdentityFallback = + NumberFormatOptions::RangeIdentityFallback::Approximately; + + mozilla::Result<mozilla::UniquePtr<Formatter>, mozilla::intl::ICUError> + result = Formatter::TryCreate(locale.get(), options); + + if (result.isOk()) { + return result.unwrap().release(); + } + + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; +} + +static mozilla::intl::NumberFormat* GetOrCreateNumberFormat( + JSContext* cx, Handle<NumberFormatObject*> numberFormat) { + // Obtain a cached mozilla::intl::NumberFormat object. + mozilla::intl::NumberFormat* nf = numberFormat->getNumberFormatter(); + if (nf) { + return nf; + } + + nf = NewNumberFormat<mozilla::intl::NumberFormat>(cx, numberFormat); + if (!nf) { + return nullptr; + } + numberFormat->setNumberFormatter(nf); + + intl::AddICUCellMemory(numberFormat, NumberFormatObject::EstimatedMemoryUse); + return nf; +} + +static mozilla::intl::NumberRangeFormat* GetOrCreateNumberRangeFormat( + JSContext* cx, Handle<NumberFormatObject*> numberFormat) { + // Obtain a cached mozilla::intl::NumberRangeFormat object. + mozilla::intl::NumberRangeFormat* nrf = + numberFormat->getNumberRangeFormatter(); + if (nrf) { + return nrf; + } + + nrf = NewNumberFormat<mozilla::intl::NumberRangeFormat>(cx, numberFormat); + if (!nrf) { + return nullptr; + } + numberFormat->setNumberRangeFormatter(nrf); + + intl::AddICUCellMemory(numberFormat, + NumberFormatObject::EstimatedRangeFormatterMemoryUse); + return nrf; +} + +static FieldType GetFieldTypeForNumberPartType( + mozilla::intl::NumberPartType type) { + switch (type) { + case mozilla::intl::NumberPartType::ApproximatelySign: + return &JSAtomState::approximatelySign; + case mozilla::intl::NumberPartType::Compact: + return &JSAtomState::compact; + case mozilla::intl::NumberPartType::Currency: + return &JSAtomState::currency; + case mozilla::intl::NumberPartType::Decimal: + return &JSAtomState::decimal; + case mozilla::intl::NumberPartType::ExponentInteger: + return &JSAtomState::exponentInteger; + case mozilla::intl::NumberPartType::ExponentMinusSign: + return &JSAtomState::exponentMinusSign; + case mozilla::intl::NumberPartType::ExponentSeparator: + return &JSAtomState::exponentSeparator; + case mozilla::intl::NumberPartType::Fraction: + return &JSAtomState::fraction; + case mozilla::intl::NumberPartType::Group: + return &JSAtomState::group; + case mozilla::intl::NumberPartType::Infinity: + return &JSAtomState::infinity; + case mozilla::intl::NumberPartType::Integer: + return &JSAtomState::integer; + case mozilla::intl::NumberPartType::Literal: + return &JSAtomState::literal; + case mozilla::intl::NumberPartType::MinusSign: + return &JSAtomState::minusSign; + case mozilla::intl::NumberPartType::Nan: + return &JSAtomState::nan; + case mozilla::intl::NumberPartType::Percent: + return &JSAtomState::percentSign; + case mozilla::intl::NumberPartType::PlusSign: + return &JSAtomState::plusSign; + case mozilla::intl::NumberPartType::Unit: + return &JSAtomState::unit; + } + + MOZ_ASSERT_UNREACHABLE( + "unenumerated, undocumented format field returned by iterator"); + return nullptr; +} + +static FieldType GetFieldTypeForNumberPartSource( + mozilla::intl::NumberPartSource source) { + switch (source) { + case mozilla::intl::NumberPartSource::Shared: + return &JSAtomState::shared; + case mozilla::intl::NumberPartSource::Start: + return &JSAtomState::startRange; + case mozilla::intl::NumberPartSource::End: + return &JSAtomState::endRange; + } + + MOZ_CRASH("unexpected number part source"); +} + +enum class DisplayNumberPartSource : bool { No, Yes }; + +static bool FormattedNumberToParts(JSContext* cx, HandleString str, + const mozilla::intl::NumberPartVector& parts, + DisplayNumberPartSource displaySource, + FieldType unitType, + MutableHandleValue result) { + size_t lastEndIndex = 0; + + RootedObject singlePart(cx); + RootedValue propVal(cx); + + Rooted<ArrayObject*> partsArray( + cx, NewDenseFullyAllocatedArray(cx, parts.length())); + if (!partsArray) { + return false; + } + partsArray->ensureDenseInitializedLength(0, parts.length()); + + size_t index = 0; + for (const auto& part : parts) { + FieldType type = GetFieldTypeForNumberPartType(part.type); + size_t endIndex = part.endIndex; + + MOZ_ASSERT(lastEndIndex < endIndex); + + singlePart = NewPlainObject(cx); + if (!singlePart) { + return false; + } + + propVal.setString(cx->names().*type); + if (!DefineDataProperty(cx, singlePart, cx->names().type, propVal)) { + return false; + } + + JSLinearString* partSubstr = + NewDependentString(cx, str, lastEndIndex, endIndex - lastEndIndex); + if (!partSubstr) { + return false; + } + + propVal.setString(partSubstr); + if (!DefineDataProperty(cx, singlePart, cx->names().value, propVal)) { + return false; + } + + if (displaySource == DisplayNumberPartSource::Yes) { + FieldType source = GetFieldTypeForNumberPartSource(part.source); + + propVal.setString(cx->names().*source); + if (!DefineDataProperty(cx, singlePart, cx->names().source, propVal)) { + return false; + } + } + + if (unitType != nullptr && type != &JSAtomState::literal) { + propVal.setString(cx->names().*unitType); + if (!DefineDataProperty(cx, singlePart, cx->names().unit, propVal)) { + return false; + } + } + + partsArray->initDenseElement(index++, ObjectValue(*singlePart)); + + lastEndIndex = endIndex; + } + + MOZ_ASSERT(index == parts.length()); + MOZ_ASSERT(lastEndIndex == str->length(), + "result array must partition the entire string"); + + result.setObject(*partsArray); + return true; +} + +bool js::intl::FormattedRelativeTimeToParts( + JSContext* cx, HandleString str, + const mozilla::intl::NumberPartVector& parts, FieldType relativeTimeUnit, + MutableHandleValue result) { + return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::No, + relativeTimeUnit, result); +} + +// Return true if the string starts with "0[bBoOxX]", possibly skipping over +// leading whitespace. +template <typename CharT> +static bool IsNonDecimalNumber(mozilla::Range<const CharT> chars) { + const CharT* end = chars.begin().get() + chars.length(); + const CharT* start = SkipSpace(chars.begin().get(), end); + + if (end - start >= 2 && start[0] == '0') { + CharT ch = start[1]; + return ch == 'b' || ch == 'B' || ch == 'o' || ch == 'O' || ch == 'x' || + ch == 'X'; + } + return false; +} + +static bool IsNonDecimalNumber(JSLinearString* str) { + JS::AutoCheckCannotGC nogc; + return str->hasLatin1Chars() ? IsNonDecimalNumber(str->latin1Range(nogc)) + : IsNonDecimalNumber(str->twoByteRange(nogc)); +} + +static bool ToIntlMathematicalValue(JSContext* cx, MutableHandleValue value) { + if (!ToPrimitive(cx, JSTYPE_NUMBER, value)) { + return false; + } + + // Maximum exponent supported by ICU. Exponents larger than this value will + // cause ICU to report an error. + // See also "intl/icu/source/i18n/decContext.h". + constexpr int32_t maximumExponent = 999'999'999; + + // We further limit the maximum positive exponent to avoid spending multiple + // seconds or even minutes in ICU when formatting large numbers. + constexpr int32_t maximumPositiveExponent = 9'999'999; + + // Compute the maximum BigInt digit length from the maximum positive exponent. + // + // BigInts are stored with base |2 ** BigInt::DigitBits|, so we have: + // + // |maximumPositiveExponent| * Log_DigitBase(10) + // = |maximumPositiveExponent| * Log2(10) / Log2(2 ** BigInt::DigitBits) + // = |maximumPositiveExponent| * Log2(10) / BigInt::DigitBits + // = 33219277.626945525... / BigInt::DigitBits + constexpr size_t maximumBigIntLength = 33219277.626945525 / BigInt::DigitBits; + + if (!value.isString()) { + if (!ToNumeric(cx, value)) { + return false; + } + + if (value.isBigInt() && + value.toBigInt()->digitLength() > maximumBigIntLength) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_EXPONENT_TOO_LARGE); + return false; + } + + return true; + } + + JSLinearString* str = value.toString()->ensureLinear(cx); + if (!str) { + return false; + } + + // Parse the string as a number. + double number = LinearStringToNumber(str); + + bool exponentTooLarge = false; + if (std::isnan(number)) { + // Set to NaN if the input can't be parsed as a number. + value.setNaN(); + } else if (IsNonDecimalNumber(str)) { + // ICU doesn't accept non-decimal numbers, so we have to convert the input + // into a base-10 string. + + MOZ_ASSERT(!mozilla::IsNegative(number), + "non-decimal numbers can't be negative"); + + if (number < DOUBLE_INTEGRAL_PRECISION_LIMIT) { + // Fast-path if we can guarantee there was no loss of precision. + value.setDouble(number); + } else { + // For the slow-path convert the string into a BigInt. + + // StringToBigInt can't fail (other than OOM) when StringToNumber already + // succeeded. + RootedString rooted(cx, str); + BigInt* bi; + JS_TRY_VAR_OR_RETURN_FALSE(cx, bi, StringToBigInt(cx, rooted)); + MOZ_ASSERT(bi); + + if (bi->digitLength() > maximumBigIntLength) { + exponentTooLarge = true; + } else { + value.setBigInt(bi); + } + } + } else { + JS::AutoCheckCannotGC nogc; + if (auto decimal = intl::DecimalNumber::from(str, nogc)) { + if (decimal->isZero()) { + // Normalize positive/negative zero. + MOZ_ASSERT(number == 0); + + value.setDouble(number); + } else if (decimal->exponentTooLarge() || + std::abs(decimal->exponent()) >= maximumExponent || + decimal->exponent() > maximumPositiveExponent) { + exponentTooLarge = true; + } + } else { + // If we can't parse the string as a decimal, it must be ±Infinity. + MOZ_ASSERT(std::isinf(number)); + MOZ_ASSERT(StringFindPattern(str, cx->names().Infinity, 0) >= 0); + + value.setDouble(number); + } + } + + if (exponentTooLarge) { + // Throw an error if the exponent is too large. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_EXPONENT_TOO_LARGE); + return false; + } + + return true; +} + +// Return the number part of the input by removing leading and trailing +// whitespace. +template <typename CharT> +static mozilla::Span<const CharT> NumberPart(const CharT* chars, + size_t length) { + const CharT* start = chars; + const CharT* end = chars + length; + + start = SkipSpace(start, end); + + // |SkipSpace| only supports forward iteration, so inline the backwards + // iteration here. + MOZ_ASSERT(start <= end); + while (end > start && unicode::IsSpace(end[-1])) { + end--; + } + + // The number part is a non-empty, ASCII-only substring. + MOZ_ASSERT(start < end); + MOZ_ASSERT(mozilla::IsAscii(mozilla::Span(start, end))); + + return {start, end}; +} + +static bool NumberPart(JSContext* cx, JSLinearString* str, + const JS::AutoCheckCannotGC& nogc, + JS::UniqueChars& latin1, std::string_view& result) { + if (str->hasLatin1Chars()) { + auto span = NumberPart( + reinterpret_cast<const char*>(str->latin1Chars(nogc)), str->length()); + + result = {span.data(), span.size()}; + return true; + } + + auto span = NumberPart(str->twoByteChars(nogc), str->length()); + + latin1.reset(JS::LossyTwoByteCharsToNewLatin1CharsZ(cx, span).c_str()); + if (!latin1) { + return false; + } + + result = {latin1.get(), span.size()}; + return true; +} + +bool js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args[0].isObject()); +#ifndef NIGHTLY_BUILD + MOZ_ASSERT(args[1].isNumeric()); +#endif + MOZ_ASSERT(args[2].isBoolean()); + + Rooted<NumberFormatObject*> numberFormat( + cx, &args[0].toObject().as<NumberFormatObject>()); + + RootedValue value(cx, args[1]); +#ifdef NIGHTLY_BUILD + if (!ToIntlMathematicalValue(cx, &value)) { + return false; + } +#endif + + mozilla::intl::NumberFormat* nf = GetOrCreateNumberFormat(cx, numberFormat); + if (!nf) { + return false; + } + + // Actually format the number + using ICUError = mozilla::intl::ICUError; + + bool formatToParts = args[2].toBoolean(); + mozilla::Result<std::u16string_view, ICUError> result = + mozilla::Err(ICUError::InternalError); + mozilla::intl::NumberPartVector parts; + if (value.isNumber()) { + double num = value.toNumber(); + if (formatToParts) { + result = nf->formatToParts(num, parts); + } else { + result = nf->format(num); + } + } else if (value.isBigInt()) { + RootedBigInt bi(cx, value.toBigInt()); + + int64_t num; + if (BigInt::isInt64(bi, &num)) { + if (formatToParts) { + result = nf->formatToParts(num, parts); + } else { + result = nf->format(num); + } + } else { + JSLinearString* str = BigInt::toString<CanGC>(cx, bi, 10); + if (!str) { + return false; + } + MOZ_RELEASE_ASSERT(str->hasLatin1Chars()); + + JS::AutoCheckCannotGC nogc; + + const char* chars = reinterpret_cast<const char*>(str->latin1Chars(nogc)); + if (formatToParts) { + result = + nf->formatToParts(std::string_view(chars, str->length()), parts); + } else { + result = nf->format(std::string_view(chars, str->length())); + } + } + } else { + JSLinearString* str = value.toString()->ensureLinear(cx); + if (!str) { + return false; + } + + JS::AutoCheckCannotGC nogc; + + // Two-byte strings have to be copied into a separate |char| buffer. + JS::UniqueChars latin1; + + std::string_view sv; + if (!NumberPart(cx, str, nogc, latin1, sv)) { + return false; + } + + if (formatToParts) { + result = nf->formatToParts(sv, parts); + } else { + result = nf->format(sv); + } + } + + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + RootedString str(cx, NewStringCopy<CanGC>(cx, result.unwrap())); + if (!str) { + return false; + } + + if (formatToParts) { + return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::No, + nullptr, args.rval()); + } + + args.rval().setString(str); + return true; +} + +static JSLinearString* ToLinearString(JSContext* cx, HandleValue val) { + // Special case to preserve negative zero. + if (val.isDouble() && mozilla::IsNegativeZero(val.toDouble())) { + constexpr std::string_view negativeZero = "-0"; + return NewStringCopy<CanGC>(cx, negativeZero); + } + + JSString* str = ToString(cx, val); + return str ? str->ensureLinear(cx) : nullptr; +}; + +bool js::intl_FormatNumberRange(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 4); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(!args[1].isUndefined()); + MOZ_ASSERT(!args[2].isUndefined()); + MOZ_ASSERT(args[3].isBoolean()); + + Rooted<NumberFormatObject*> numberFormat( + cx, &args[0].toObject().as<NumberFormatObject>()); + bool formatToParts = args[3].toBoolean(); + + RootedValue start(cx, args[1]); + if (!ToIntlMathematicalValue(cx, &start)) { + return false; + } + + RootedValue end(cx, args[2]); + if (!ToIntlMathematicalValue(cx, &end)) { + return false; + } + + // PartitionNumberRangePattern, step 1. + if (start.isDouble() && std::isnan(start.toDouble())) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_NAN_NUMBER_RANGE, "start", + "NumberFormat", formatToParts ? "formatRangeToParts" : "formatRange"); + return false; + } + if (end.isDouble() && std::isnan(end.toDouble())) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_NAN_NUMBER_RANGE, "end", + "NumberFormat", formatToParts ? "formatRangeToParts" : "formatRange"); + return false; + } + + using NumberRangeFormat = mozilla::intl::NumberRangeFormat; + NumberRangeFormat* nf = GetOrCreateNumberRangeFormat(cx, numberFormat); + if (!nf) { + return false; + } + + auto valueRepresentableAsDouble = [](const Value& val, double* num) { + if (val.isNumber()) { + *num = val.toNumber(); + return true; + } + if (val.isBigInt()) { + int64_t i64; + if (BigInt::isInt64(val.toBigInt(), &i64) && + i64 < int64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT) && + i64 > -int64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)) { + *num = double(i64); + return true; + } + } + return false; + }; + + // Actually format the number range. + using ICUError = mozilla::intl::ICUError; + + mozilla::Result<std::u16string_view, ICUError> result = + mozilla::Err(ICUError::InternalError); + mozilla::intl::NumberPartVector parts; + + double numStart, numEnd; + if (valueRepresentableAsDouble(start, &numStart) && + valueRepresentableAsDouble(end, &numEnd)) { + if (formatToParts) { + result = nf->formatToParts(numStart, numEnd, parts); + } else { + result = nf->format(numStart, numEnd); + } + } else { + Rooted<JSLinearString*> strStart(cx, ToLinearString(cx, start)); + if (!strStart) { + return false; + } + + Rooted<JSLinearString*> strEnd(cx, ToLinearString(cx, end)); + if (!strEnd) { + return false; + } + + JS::AutoCheckCannotGC nogc; + + // Two-byte strings have to be copied into a separate |char| buffer. + JS::UniqueChars latin1Start; + JS::UniqueChars latin1End; + + std::string_view svStart; + if (!NumberPart(cx, strStart, nogc, latin1Start, svStart)) { + return false; + } + + std::string_view svEnd; + if (!NumberPart(cx, strEnd, nogc, latin1End, svEnd)) { + return false; + } + + if (formatToParts) { + result = nf->formatToParts(svStart, svEnd, parts); + } else { + result = nf->format(svStart, svEnd); + } + } + + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + RootedString str(cx, NewStringCopy<CanGC>(cx, result.unwrap())); + if (!str) { + return false; + } + + if (formatToParts) { + return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::Yes, + nullptr, args.rval()); + } + + args.rval().setString(str); + return true; +} diff --git a/js/src/builtin/intl/NumberFormat.h b/js/src/builtin/intl/NumberFormat.h new file mode 100644 index 0000000000..a4d552510f --- /dev/null +++ b/js/src/builtin/intl/NumberFormat.h @@ -0,0 +1,129 @@ +/* -*- 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 builtin_intl_NumberFormat_h +#define builtin_intl_NumberFormat_h + +#include <stdint.h> + +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "vm/NativeObject.h" + +namespace mozilla::intl { +class NumberFormat; +class NumberRangeFormat; +} // namespace mozilla::intl + +namespace js { + +class NumberFormatObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t UNUMBER_FORMATTER_SLOT = 1; + static constexpr uint32_t UNUMBER_RANGE_FORMATTER_SLOT = 2; + static constexpr uint32_t SLOT_COUNT = 3; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals " + "object slot"); + + // Estimated memory use for UNumberFormatter and UFormattedNumber + // (see IcuMemoryUsage). + static constexpr size_t EstimatedMemoryUse = 972; + + // Estimated memory use for UNumberRangeFormatter and UFormattedNumberRange + // (see IcuMemoryUsage). + static constexpr size_t EstimatedRangeFormatterMemoryUse = 19894; + + mozilla::intl::NumberFormat* getNumberFormatter() const { + const auto& slot = getFixedSlot(UNUMBER_FORMATTER_SLOT); + if (slot.isUndefined()) { + return nullptr; + } + return static_cast<mozilla::intl::NumberFormat*>(slot.toPrivate()); + } + + void setNumberFormatter(mozilla::intl::NumberFormat* formatter) { + setFixedSlot(UNUMBER_FORMATTER_SLOT, PrivateValue(formatter)); + } + + mozilla::intl::NumberRangeFormat* getNumberRangeFormatter() const { + const auto& slot = getFixedSlot(UNUMBER_RANGE_FORMATTER_SLOT); + if (slot.isUndefined()) { + return nullptr; + } + return static_cast<mozilla::intl::NumberRangeFormat*>(slot.toPrivate()); + } + + void setNumberRangeFormatter(mozilla::intl::NumberRangeFormat* formatter) { + setFixedSlot(UNUMBER_RANGE_FORMATTER_SLOT, PrivateValue(formatter)); + } + + private: + static const JSClassOps classOps_; + static const ClassSpec classSpec_; + + static void finalize(JS::GCContext* gcx, JSObject* obj); +}; + +/** + * Returns a new instance of the standard built-in NumberFormat constructor. + * Self-hosted code cannot cache this constructor (as it does for others in + * Utilities.js) because it is initialized after self-hosted code is compiled. + * + * Usage: numberFormat = intl_NumberFormat(locales, options) + */ +[[nodiscard]] extern bool intl_NumberFormat(JSContext* cx, unsigned argc, + Value* vp); + +/** + * Returns the numbering system type identifier per Unicode + * Technical Standard 35, Unicode Locale Data Markup Language, for the + * default numbering system for the given locale. + * + * Usage: defaultNumberingSystem = intl_numberingSystem(locale) + */ +[[nodiscard]] extern bool intl_numberingSystem(JSContext* cx, unsigned argc, + Value* vp); + +/** + * Returns a string representing the number x according to the effective + * locale and the formatting options of the given NumberFormat. + * + * Spec: ECMAScript Internationalization API Specification, 11.3.2. + * + * Usage: formatted = intl_FormatNumber(numberFormat, x, formatToParts) + */ +[[nodiscard]] extern bool intl_FormatNumber(JSContext* cx, unsigned argc, + Value* vp); + +/** + * Returns a string representing the number range «x - y» according to the + * effective locale and the formatting options of the given NumberFormat. + * + * Usage: formatted = intl_FormatNumberRange(numberFormat, x, y, formatToParts) + */ +[[nodiscard]] extern bool intl_FormatNumberRange(JSContext* cx, unsigned argc, + Value* vp); + +#if DEBUG || MOZ_SYSTEM_ICU +/** + * Returns an object with all available measurement units. + * + * Usage: units = intl_availableMeasurementUnits() + */ +[[nodiscard]] extern bool intl_availableMeasurementUnits(JSContext* cx, + unsigned argc, + Value* vp); +#endif + +} // namespace js + +#endif /* builtin_intl_NumberFormat_h */ diff --git a/js/src/builtin/intl/NumberFormat.js b/js/src/builtin/intl/NumberFormat.js new file mode 100644 index 0000000000..b668bd5058 --- /dev/null +++ b/js/src/builtin/intl/NumberFormat.js @@ -0,0 +1,1210 @@ +/* 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/. */ + +/* Portions Copyright Norbert Lindenberg 2011-2012. */ + +#include "NumberingSystemsGenerated.h" + +/** + * NumberFormat internal properties. + * + * Spec: ECMAScript Internationalization API Specification, 9.1 and 11.3.3. + */ +var numberFormatInternalProperties = { + localeData: numberFormatLocaleData, + relevantExtensionKeys: ["nu"], +}; + +/** + * Compute an internal properties object from |lazyNumberFormatData|. + */ +function resolveNumberFormatInternals(lazyNumberFormatData) { + assert(IsObject(lazyNumberFormatData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + var NumberFormat = numberFormatInternalProperties; + + // Compute effective locale. + + // Step 7. + var localeData = NumberFormat.localeData; + + // Step 8. + var r = ResolveLocale( + "NumberFormat", + lazyNumberFormatData.requestedLocales, + lazyNumberFormatData.opt, + NumberFormat.relevantExtensionKeys, + localeData + ); + + // Steps 9-10. (Step 11 is not relevant to our implementation.) + internalProps.locale = r.locale; + internalProps.numberingSystem = r.nu; + + // Compute formatting options. + // Step 13. + var style = lazyNumberFormatData.style; + internalProps.style = style; + + // Steps 17, 19. + if (style === "currency") { + internalProps.currency = lazyNumberFormatData.currency; + internalProps.currencyDisplay = lazyNumberFormatData.currencyDisplay; + internalProps.currencySign = lazyNumberFormatData.currencySign; + } + + // Intl.NumberFormat Unified API Proposal + if (style === "unit") { + internalProps.unit = lazyNumberFormatData.unit; + internalProps.unitDisplay = lazyNumberFormatData.unitDisplay; + } + + // Intl.NumberFormat Unified API Proposal + var notation = lazyNumberFormatData.notation; + internalProps.notation = notation; + + // Step 22. + internalProps.minimumIntegerDigits = + lazyNumberFormatData.minimumIntegerDigits; + + if ("minimumFractionDigits" in lazyNumberFormatData) { + // Note: Intl.NumberFormat.prototype.resolvedOptions() exposes the + // actual presence (versus undefined-ness) of these properties. + assert( + "maximumFractionDigits" in lazyNumberFormatData, + "min/max frac digits mismatch" + ); + internalProps.minimumFractionDigits = + lazyNumberFormatData.minimumFractionDigits; + internalProps.maximumFractionDigits = + lazyNumberFormatData.maximumFractionDigits; + } + + if ("minimumSignificantDigits" in lazyNumberFormatData) { + // Note: Intl.NumberFormat.prototype.resolvedOptions() exposes the + // actual presence (versus undefined-ness) of these properties. + assert( + "maximumSignificantDigits" in lazyNumberFormatData, + "min/max sig digits mismatch" + ); + internalProps.minimumSignificantDigits = + lazyNumberFormatData.minimumSignificantDigits; + internalProps.maximumSignificantDigits = + lazyNumberFormatData.maximumSignificantDigits; + } + + // Intl.NumberFormat v3 Proposal + internalProps.trailingZeroDisplay = lazyNumberFormatData.trailingZeroDisplay; + internalProps.roundingIncrement = lazyNumberFormatData.roundingIncrement; + + // Intl.NumberFormat Unified API Proposal + if (notation === "compact") { + internalProps.compactDisplay = lazyNumberFormatData.compactDisplay; + } + + // Step 24. + internalProps.useGrouping = lazyNumberFormatData.useGrouping; + + // Intl.NumberFormat Unified API Proposal + internalProps.signDisplay = lazyNumberFormatData.signDisplay; + + // Intl.NumberFormat v3 Proposal + internalProps.roundingMode = lazyNumberFormatData.roundingMode; + + // Intl.NumberFormat v3 Proposal + internalProps.roundingPriority = lazyNumberFormatData.roundingPriority; + + // The caller is responsible for associating |internalProps| with the right + // object using |setInternalProperties|. + return internalProps; +} + +/** + * Returns an object containing the NumberFormat internal properties of |obj|. + */ +function getNumberFormatInternals(obj) { + assert(IsObject(obj), "getNumberFormatInternals called with non-object"); + assert( + intl_GuardToNumberFormat(obj) !== null, + "getNumberFormatInternals called with non-NumberFormat" + ); + + var internals = getIntlObjectInternals(obj); + assert( + internals.type === "NumberFormat", + "bad type escaped getIntlObjectInternals" + ); + + // If internal properties have already been computed, use them. + var internalProps = maybeInternalProperties(internals); + if (internalProps) { + return internalProps; + } + + // Otherwise it's time to fully create them. + internalProps = resolveNumberFormatInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * 11.1.11 UnwrapNumberFormat( nf ) + */ +function UnwrapNumberFormat(nf) { + // Steps 2 and 4 (error handling moved to caller). + if ( + IsObject(nf) && + intl_GuardToNumberFormat(nf) === null && + !intl_IsWrappedNumberFormat(nf) && + callFunction( + std_Object_isPrototypeOf, + GetBuiltinPrototype("NumberFormat"), + nf + ) + ) { + nf = nf[intlFallbackSymbol()]; + } + return nf; +} + +/** + * Applies digit options used for number formatting onto the intl object. + * + * Spec: ECMAScript Internationalization API Specification, 11.1.1. + */ +function SetNumberFormatDigitOptions( + lazyData, + options, + mnfdDefault, + mxfdDefault, + notation +) { + // We skip step 1 because we set the properties on a lazyData object. + + // Steps 2-4. + assert(IsObject(options), "SetNumberFormatDigitOptions"); + assert(typeof mnfdDefault === "number", "SetNumberFormatDigitOptions"); + assert(typeof mxfdDefault === "number", "SetNumberFormatDigitOptions"); + assert(mnfdDefault <= mxfdDefault, "SetNumberFormatDigitOptions"); + assert(typeof notation === "string", "SetNumberFormatDigitOptions"); + + // Steps 5-9. + const mnid = GetNumberOption(options, "minimumIntegerDigits", 1, 21, 1); + let mnfd = options.minimumFractionDigits; + let mxfd = options.maximumFractionDigits; + let mnsd = options.minimumSignificantDigits; + let mxsd = options.maximumSignificantDigits; + + // Step 10. + lazyData.minimumIntegerDigits = mnid; + +#ifdef NIGHTLY_BUILD + // Intl.NumberFormat v3 Proposal + var roundingPriority = GetOption( + options, + "roundingPriority", + "string", + ["auto", "morePrecision", "lessPrecision"], + "auto" + ); +#else + var roundingPriority = "auto"; +#endif + + const hasSignificantDigits = mnsd !== undefined || mxsd !== undefined; + const hasFractionDigits = mnfd !== undefined || mxfd !== undefined; + + const needSignificantDigits = + roundingPriority !== "auto" || hasSignificantDigits; + const needFractionalDigits = + roundingPriority !== "auto" || + !(hasSignificantDigits || (!hasFractionDigits && notation === "compact")); + + if (needSignificantDigits) { + // Step 11. + if (hasSignificantDigits) { + // Step 11.a (Omitted). + + // Step 11.b. + mnsd = DefaultNumberOption(mnsd, 1, 21, 1); + + // Step 11.c. + mxsd = DefaultNumberOption(mxsd, mnsd, 21, 21); + + // Step 11.d. + lazyData.minimumSignificantDigits = mnsd; + + // Step 11.e. + lazyData.maximumSignificantDigits = mxsd; + } else { + lazyData.minimumSignificantDigits = 1; + lazyData.maximumSignificantDigits = 21; + } + } + + if (needFractionalDigits) { + // Step 12. + if (hasFractionDigits) { + // Step 12.a (Omitted). + + // Step 12.b. + mnfd = DefaultNumberOption(mnfd, 0, 20, undefined); + + // Step 12.c. + mxfd = DefaultNumberOption(mxfd, 0, 20, undefined); + + // Step 12.d. + if (mnfd === undefined) { + assert( + mxfd !== undefined, + "mxfd isn't undefined when mnfd is undefined" + ); + mnfd = std_Math_min(mnfdDefault, mxfd); + } + + // Step 12.e. + else if (mxfd === undefined) { + mxfd = std_Math_max(mxfdDefault, mnfd); + } + + // Step 12.f. + else if (mnfd > mxfd) { + ThrowRangeError(JSMSG_INVALID_DIGITS_VALUE, mxfd); + } + + // Step 12.g. + lazyData.minimumFractionDigits = mnfd; + + // Step 12.h. + lazyData.maximumFractionDigits = mxfd; + } else { + // Step 14.a (Omitted). + + // Step 14.b. + lazyData.minimumFractionDigits = mnfdDefault; + + // Step 14.c. + lazyData.maximumFractionDigits = mxfdDefault; + } + } + + if (needSignificantDigits || needFractionalDigits) { + lazyData.roundingPriority = roundingPriority; + } else { + assert(!hasSignificantDigits, "bad significant digits in fallback case"); + assert( + roundingPriority === "auto", + `bad rounding in fallback case: ${roundingPriority}` + ); + assert( + notation === "compact", + `bad notation in fallback case: ${notation}` + ); + + lazyData.roundingPriority = "morePrecision"; + lazyData.minimumFractionDigits = 0; + lazyData.maximumFractionDigits = 0; + lazyData.minimumSignificantDigits = 1; + lazyData.maximumSignificantDigits = 2; + } +} + +/** + * Convert s to upper case, but limited to characters a-z. + * + * Spec: ECMAScript Internationalization API Specification, 6.1. + */ +function toASCIIUpperCase(s) { + assert(typeof s === "string", "toASCIIUpperCase"); + + // String.prototype.toUpperCase may map non-ASCII characters into ASCII, + // so go character by character (actually code unit by code unit, but + // since we only care about ASCII characters here, that's OK). + var result = ""; + for (var i = 0; i < s.length; i++) { + var c = callFunction(std_String_charCodeAt, s, i); + result += + 0x61 <= c && c <= 0x7a + ? callFunction(std_String_fromCharCode, null, c & ~0x20) + : s[i]; + } + return result; +} + +/** + * Verifies that the given string is a well-formed ISO 4217 currency code. + * + * Spec: ECMAScript Internationalization API Specification, 6.3.1. + */ +function IsWellFormedCurrencyCode(currency) { + assert(typeof currency === "string", "currency is a string value"); + + return currency.length === 3 && IsASCIIAlphaString(currency); +} + +/** + * Verifies that the given string is a well-formed core unit identifier as + * defined in UTS #35, Part 2, Section 6. In addition to obeying the UTS #35 + * core unit identifier syntax, |unitIdentifier| must be one of the identifiers + * sanctioned by UTS #35 or be a compound unit composed of two sanctioned simple + * units. + * + * Intl.NumberFormat Unified API Proposal + */ +function IsWellFormedUnitIdentifier(unitIdentifier) { + assert( + typeof unitIdentifier === "string", + "unitIdentifier is a string value" + ); + + // Step 1. + if (IsSanctionedSimpleUnitIdentifier(unitIdentifier)) { + return true; + } + + // Step 2. + var pos = callFunction(std_String_indexOf, unitIdentifier, "-per-"); + if (pos < 0) { + return false; + } + + var next = pos + "-per-".length; + + // Steps 3 and 5. + var numerator = Substring(unitIdentifier, 0, pos); + var denominator = Substring( + unitIdentifier, + next, + unitIdentifier.length - next + ); + + // Steps 4 and 6. + return ( + IsSanctionedSimpleUnitIdentifier(numerator) && + IsSanctionedSimpleUnitIdentifier(denominator) + ); +} + +#if DEBUG || MOZ_SYSTEM_ICU +var availableMeasurementUnits = { + value: null, +}; +#endif + +/** + * Verifies that the given string is a sanctioned simple core unit identifier. + * + * Intl.NumberFormat Unified API Proposal + * + * Also see: https://unicode.org/reports/tr35/tr35-general.html#Unit_Elements + */ +function IsSanctionedSimpleUnitIdentifier(unitIdentifier) { + assert( + typeof unitIdentifier === "string", + "unitIdentifier is a string value" + ); + + var isSanctioned = hasOwn(unitIdentifier, sanctionedSimpleUnitIdentifiers); + +#if DEBUG || MOZ_SYSTEM_ICU + if (isSanctioned) { + if (availableMeasurementUnits.value === null) { + availableMeasurementUnits.value = intl_availableMeasurementUnits(); + } + + var isSupported = hasOwn(unitIdentifier, availableMeasurementUnits.value); + +#if MOZ_SYSTEM_ICU + // A system ICU may support fewer measurement units, so we need to make + // sure the unit is actually supported. + isSanctioned = isSupported; +#else + // Otherwise just assert that the sanctioned unit is also supported. + assert( + isSupported, + `"${unitIdentifier}" is sanctioned but not supported. Did you forget to update + intl/icu/data_filter.json to include the unit (and any implicit compound units)? + For example "speed/kilometer-per-hour" is implied by "length/kilometer" and + "duration/hour" and must therefore also be present.` + ); +#endif + } +#endif + + return isSanctioned; +} + +/* eslint-disable complexity */ +/** + * Initializes an object as a NumberFormat. + * + * This method is complicated a moderate bit by its implementing initialization + * as a *lazy* concept. Everything that must happen now, does -- but we defer + * all the work we can until the object is actually used as a NumberFormat. + * This later work occurs in |resolveNumberFormatInternals|; steps not noted + * here occur there. + * + * Spec: ECMAScript Internationalization API Specification, 11.1.2. + */ +function InitializeNumberFormat(numberFormat, thisValue, locales, options) { + assert( + IsObject(numberFormat), + "InitializeNumberFormat called with non-object" + ); + assert( + intl_GuardToNumberFormat(numberFormat) !== null, + "InitializeNumberFormat called with non-NumberFormat" + ); + + // Lazy NumberFormat data has the following structure: + // + // { + // requestedLocales: List of locales, + // style: "decimal" / "percent" / "currency" / "unit", + // + // // fields present only if style === "currency": + // currency: a well-formed currency code (IsWellFormedCurrencyCode), + // currencyDisplay: "code" / "symbol" / "narrowSymbol" / "name", + // currencySign: "standard" / "accounting", + // + // // fields present only if style === "unit": + // unit: a well-formed unit identifier (IsWellFormedUnitIdentifier), + // unitDisplay: "short" / "narrow" / "long", + // + // opt: // opt object computed in InitializeNumberFormat + // { + // localeMatcher: "lookup" / "best fit", + // + // nu: string matching a Unicode extension type, // optional + // } + // + // minimumIntegerDigits: integer ∈ [1, 21], + // + // // optional, mutually exclusive with the significant-digits option + // minimumFractionDigits: integer ∈ [0, 20], + // maximumFractionDigits: integer ∈ [0, 20], + // + // // optional, mutually exclusive with the fraction-digits option + // minimumSignificantDigits: integer ∈ [1, 21], + // maximumSignificantDigits: integer ∈ [1, 21], + // + // roundingPriority: "auto" / "lessPrecision" / "morePrecision", + // + // // accepts different values when Intl.NumberFormat v3 proposal is enabled + // useGrouping: true / false, + // useGrouping: "auto" / "always" / "min2" / false, + // + // notation: "standard" / "scientific" / "engineering" / "compact", + // + // // optional, if notation is "compact" + // compactDisplay: "short" / "long", + // + // signDisplay: "auto" / "never" / "always" / "exceptZero", + // + // trailingZeroDisplay: "auto" / "stripIfInteger", + // + // roundingIncrement: integer ∈ (1, 2, 5, + // 10, 20, 25, 50, + // 100, 200, 250, 500, + // 1000, 2000, 2500, 5000), + // + // roundingMode: "ceil" / "floor" / "expand" / "trunc" / + // "halfCeil" / "halfFloor" / "halfExpand" / "halfTrunc" / "halfEven", + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every NumberFormat lazy data object has *all* these properties, never a + // subset of them. + var lazyNumberFormatData = std_Object_create(null); + + // Step 1. + var requestedLocales = CanonicalizeLocaleList(locales); + lazyNumberFormatData.requestedLocales = requestedLocales; + + // Steps 2-3. + // + // If we ever need more speed here at startup, we should try to detect the + // case where |options === undefined| and then directly use the default + // value for each option. For now, just keep it simple. + if (options === undefined) { + options = std_Object_create(null); + } else { + options = ToObject(options); + } + + // Compute options that impact interpretation of locale. + // Step 4. + var opt = new_Record(); + lazyNumberFormatData.opt = opt; + + // Steps 5-6. + var matcher = GetOption( + options, + "localeMatcher", + "string", + ["lookup", "best fit"], + "best fit" + ); + opt.localeMatcher = matcher; + + var numberingSystem = GetOption( + options, + "numberingSystem", + "string", + undefined, + undefined + ); + + if (numberingSystem !== undefined) { + numberingSystem = intl_ValidateAndCanonicalizeUnicodeExtensionType( + numberingSystem, + "numberingSystem", + "nu" + ); + } + + opt.nu = numberingSystem; + + // Compute formatting options. + // Step 12. + var style = GetOption( + options, + "style", + "string", + ["decimal", "percent", "currency", "unit"], + "decimal" + ); + lazyNumberFormatData.style = style; + + // Steps 14-17. + var currency = GetOption(options, "currency", "string", undefined, undefined); + + // Per the Intl.NumberFormat Unified API Proposal, this check should only + // happen for |style === "currency"|, which seems inconsistent, given that + // we normally validate all options when present, even the ones which are + // unused. + // TODO: File issue at <https://github.com/tc39/proposal-unified-intl-numberformat>. + if (currency !== undefined && !IsWellFormedCurrencyCode(currency)) { + ThrowRangeError(JSMSG_INVALID_CURRENCY_CODE, currency); + } + + var cDigits; + if (style === "currency") { + if (currency === undefined) { + ThrowTypeError(JSMSG_UNDEFINED_CURRENCY); + } + + // Steps 19.a-c. + currency = toASCIIUpperCase(currency); + lazyNumberFormatData.currency = currency; + cDigits = CurrencyDigits(currency); + } + + // Step 18. + var currencyDisplay = GetOption( + options, + "currencyDisplay", + "string", + ["code", "symbol", "narrowSymbol", "name"], + "symbol" + ); + if (style === "currency") { + lazyNumberFormatData.currencyDisplay = currencyDisplay; + } + + // Intl.NumberFormat Unified API Proposal + var currencySign = GetOption( + options, + "currencySign", + "string", + ["standard", "accounting"], + "standard" + ); + if (style === "currency") { + lazyNumberFormatData.currencySign = currencySign; + } + + // Intl.NumberFormat Unified API Proposal + var unit = GetOption(options, "unit", "string", undefined, undefined); + + // Aligned with |currency| check from above, see note about spec issue there. + if (unit !== undefined && !IsWellFormedUnitIdentifier(unit)) { + ThrowRangeError(JSMSG_INVALID_UNIT_IDENTIFIER, unit); + } + + var unitDisplay = GetOption( + options, + "unitDisplay", + "string", + ["short", "narrow", "long"], + "short" + ); + + if (style === "unit") { + if (unit === undefined) { + ThrowTypeError(JSMSG_UNDEFINED_UNIT); + } + + lazyNumberFormatData.unit = unit; + lazyNumberFormatData.unitDisplay = unitDisplay; + } + + // Steps 20-21. + var mnfdDefault, mxfdDefault; + if (style === "currency") { + mnfdDefault = cDigits; + mxfdDefault = cDigits; + } else { + mnfdDefault = 0; + mxfdDefault = style === "percent" ? 0 : 3; + } + + // Intl.NumberFormat Unified API Proposal + var notation = GetOption( + options, + "notation", + "string", + ["standard", "scientific", "engineering", "compact"], + "standard" + ); + lazyNumberFormatData.notation = notation; + + // Step 22. + SetNumberFormatDigitOptions( + lazyNumberFormatData, + options, + mnfdDefault, + mxfdDefault, + notation + ); + +#ifdef NIGHTLY_BUILD + // Intl.NumberFormat v3 Proposal + var roundingIncrement = GetNumberOption( + options, + "roundingIncrement", + 1, + 5000, + 1 + ); + switch (roundingIncrement) { + case 1: + case 2: + case 5: + case 10: + case 20: + case 25: + case 50: + case 100: + case 200: + case 250: + case 500: + case 1000: + case 2000: + case 2500: + case 5000: + break; + default: + ThrowRangeError( + JSMSG_INVALID_OPTION_VALUE, + "roundingIncrement", + roundingIncrement + ); + } + lazyNumberFormatData.roundingIncrement = roundingIncrement; + + if (roundingIncrement !== 1) { + // [[RoundingType]] must be `fractionDigits`. + if (lazyNumberFormatData.roundingPriority !== "auto") { + ThrowTypeError( + JSMSG_INVALID_NUMBER_OPTION, + "roundingIncrement", + "roundingPriority" + ); + } + if (hasOwn("minimumSignificantDigits", lazyNumberFormatData)) { + ThrowTypeError( + JSMSG_INVALID_NUMBER_OPTION, + "roundingIncrement", + "minimumSignificantDigits" + ); + } + + // Minimum and maximum fraction digits must be equal. + if ( + lazyNumberFormatData.minimumFractionDigits !== + lazyNumberFormatData.maximumFractionDigits + ) { + ThrowRangeError(JSMSG_UNEQUAL_FRACTION_DIGITS); + } + } +#else + lazyNumberFormatData.roundingIncrement = 1; +#endif + +#ifdef NIGHTLY_BUILD + // Intl.NumberFormat v3 Proposal + var trailingZeroDisplay = GetOption( + options, + "trailingZeroDisplay", + "string", + ["auto", "stripIfInteger"], + "auto" + ); + lazyNumberFormatData.trailingZeroDisplay = trailingZeroDisplay; +#else + lazyNumberFormatData.trailingZeroDisplay = "auto"; +#endif + + // Intl.NumberFormat Unified API Proposal + var compactDisplay = GetOption( + options, + "compactDisplay", + "string", + ["short", "long"], + "short" + ); + if (notation === "compact") { + lazyNumberFormatData.compactDisplay = compactDisplay; + } + + // Steps 23. +#ifdef NIGHTLY_BUILD + var defaultUseGrouping = notation !== "compact" ? "auto" : "min2"; + var useGrouping = GetStringOrBooleanOption( + options, + "useGrouping", + ["min2", "auto", "always"], + "always", + false, + defaultUseGrouping + ); +#else + var useGrouping = GetOption( + options, + "useGrouping", + "boolean", + undefined, + true + ); +#endif + lazyNumberFormatData.useGrouping = useGrouping; + + // Intl.NumberFormat Unified API Proposal + var signDisplay = GetOption( + options, + "signDisplay", + "string", +#ifdef NIGHTLY_BUILD + ["auto", "never", "always", "exceptZero", "negative"], +#else + ["auto", "never", "always", "exceptZero"], +#endif + "auto" + ); + lazyNumberFormatData.signDisplay = signDisplay; + +#ifdef NIGHTLY_BUILD + // Intl.NumberFormat v3 Proposal + var roundingMode = GetOption( + options, + "roundingMode", + "string", + [ + "ceil", + "floor", + "expand", + "trunc", + "halfCeil", + "halfFloor", + "halfExpand", + "halfTrunc", + "halfEven", + ], + "halfExpand" + ); + lazyNumberFormatData.roundingMode = roundingMode; +#else + lazyNumberFormatData.roundingMode = "halfExpand"; +#endif + + // Step 31. + // + // We've done everything that must be done now: mark the lazy data as fully + // computed and install it. + initializeIntlObject(numberFormat, "NumberFormat", lazyNumberFormatData); + + // 11.2.1, steps 4-5. + if ( + numberFormat !== thisValue && + callFunction( + std_Object_isPrototypeOf, + GetBuiltinPrototype("NumberFormat"), + thisValue + ) + ) { + DefineDataProperty( + thisValue, + intlFallbackSymbol(), + numberFormat, + ATTR_NONENUMERABLE | ATTR_NONCONFIGURABLE | ATTR_NONWRITABLE + ); + + return thisValue; + } + + // 11.2.1, step 6. + return numberFormat; +} +/* eslint-enable complexity */ + +/** + * Returns the number of decimal digits to be used for the given currency. + * + * Spec: ECMAScript Internationalization API Specification, 11.1.3. + */ +function CurrencyDigits(currency) { + assert(typeof currency === "string", "currency is a string value"); + assert(IsWellFormedCurrencyCode(currency), "currency is well-formed"); + assert(currency === toASCIIUpperCase(currency), "currency is all upper-case"); + + if (hasOwn(currency, currencyDigits)) { + return currencyDigits[currency]; + } + return 2; +} + +/** + * Returns the subset of the given locale list for which this locale list has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript Internationalization API Specification, 11.3.2. + */ +function Intl_NumberFormat_supportedLocalesOf(locales /*, options*/) { + var options = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 1. + var availableLocales = "NumberFormat"; + + // Step 2. + var requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. + return SupportedLocales(availableLocales, requestedLocales, options); +} + +function getNumberingSystems(locale) { + // ICU doesn't have an API to determine the set of numbering systems + // supported for a locale; it generally pretends that any numbering system + // can be used with any locale. Supporting a decimal numbering system + // (where only the digits are replaced) is easy, so we offer them all here. + // Algorithmic numbering systems are typically tied to one locale, so for + // lack of information we don't offer them. + // The one thing we can find out from ICU is the default numbering system + // for a locale. + var defaultNumberingSystem = intl_numberingSystem(locale); + return [defaultNumberingSystem, NUMBERING_SYSTEMS_WITH_SIMPLE_DIGIT_MAPPINGS]; +} + +function numberFormatLocaleData() { + return { + nu: getNumberingSystems, + default: { + nu: intl_numberingSystem, + }, + }; +} + +/** + * Create function to be cached and returned by Intl.NumberFormat.prototype.format. + * + * Spec: ECMAScript Internationalization API Specification, 11.1.4. + */ +function createNumberFormatFormat(nf) { + // This function is not inlined in $Intl_NumberFormat_format_get to avoid + // creating a call-object on each call to $Intl_NumberFormat_format_get. + return function(value) { + // Step 1 (implicit). + + // Step 2. + assert(IsObject(nf), "InitializeNumberFormat called with non-object"); + assert( + intl_GuardToNumberFormat(nf) !== null, + "InitializeNumberFormat called with non-NumberFormat" + ); + +#ifdef NIGHTLY_BUILD + var x = value; +#else + // Steps 3-4. + var x = ToNumeric(value); +#endif + + // Step 5. + return intl_FormatNumber(nf, x, /* formatToParts = */ false); + }; +} + +/** + * Returns a function bound to this NumberFormat that returns a String value + * representing the result of calling ToNumber(value) according to the + * effective locale and the formatting options of this NumberFormat. + * + * Spec: ECMAScript Internationalization API Specification, 11.4.3. + */ +// Uncloned functions with `$` prefix are allocated as extended function +// to store the original name in `SetCanonicalName`. +function $Intl_NumberFormat_format_get() { + // Steps 1-3. + var thisArg = UnwrapNumberFormat(this); + var nf = thisArg; + if (!IsObject(nf) || (nf = intl_GuardToNumberFormat(nf)) === null) { + return callFunction( + intl_CallNumberFormatMethodIfWrapped, + thisArg, + "$Intl_NumberFormat_format_get" + ); + } + + var internals = getNumberFormatInternals(nf); + + // Step 4. + if (internals.boundFormat === undefined) { + // Steps 4.a-c. + internals.boundFormat = createNumberFormatFormat(nf); + } + + // Step 5. + return internals.boundFormat; +} +SetCanonicalName($Intl_NumberFormat_format_get, "get format"); + +/** + * 11.4.4 Intl.NumberFormat.prototype.formatToParts ( value ) + */ +function Intl_NumberFormat_formatToParts(value) { + // Step 1. + var nf = this; + + // Steps 2-3. + if (!IsObject(nf) || (nf = intl_GuardToNumberFormat(nf)) === null) { + return callFunction( + intl_CallNumberFormatMethodIfWrapped, + this, + value, + "Intl_NumberFormat_formatToParts" + ); + } + +#ifdef NIGHTLY_BUILD + var x = value; +#else + // Step 4. + var x = ToNumeric(value); +#endif + + // Step 5. + return intl_FormatNumber(nf, x, /* formatToParts = */ true); +} + +/** + * Intl.NumberFormat.prototype.formatRange ( start, end ) + */ +function Intl_NumberFormat_formatRange(start, end) { + // Step 1. + var nf = this; + + // Step 2. + if (!IsObject(nf) || (nf = intl_GuardToNumberFormat(nf)) === null) { + return callFunction( + intl_CallNumberFormatMethodIfWrapped, + this, + start, + end, + "Intl_NumberFormat_formatRange" + ); + } + + // Step 3. + if (start === undefined || end === undefined) { + ThrowTypeError( + JSMSG_UNDEFINED_NUMBER, + start === undefined ? "start" : "end", + "NumberFormat", + "formatRange" + ); + } + + // Steps 4-6. + return intl_FormatNumberRange(nf, start, end, /* formatToParts = */ false); +} + +/** + * Intl.NumberFormat.prototype.formatRangeToParts ( start, end ) + */ +function Intl_NumberFormat_formatRangeToParts(start, end) { + // Step 1. + var nf = this; + + // Step 2. + if (!IsObject(nf) || (nf = intl_GuardToNumberFormat(nf)) === null) { + return callFunction( + intl_CallNumberFormatMethodIfWrapped, + this, + start, + end, + "Intl_NumberFormat_formatRangeToParts" + ); + } + + // Step 3. + if (start === undefined || end === undefined) { + ThrowTypeError( + JSMSG_UNDEFINED_NUMBER, + start === undefined ? "start" : "end", + "NumberFormat", + "formatRangeToParts" + ); + } + + // Steps 4-6. + return intl_FormatNumberRange(nf, start, end, /* formatToParts = */ true); +} + +/** + * Returns the resolved options for a NumberFormat object. + * + * Spec: ECMAScript Internationalization API Specification, 11.4.5. + */ +function Intl_NumberFormat_resolvedOptions() { + // Steps 1-3. + var thisArg = UnwrapNumberFormat(this); + var nf = thisArg; + if (!IsObject(nf) || (nf = intl_GuardToNumberFormat(nf)) === null) { + return callFunction( + intl_CallNumberFormatMethodIfWrapped, + thisArg, + "Intl_NumberFormat_resolvedOptions" + ); + } + + var internals = getNumberFormatInternals(nf); + + // Steps 4-5. + var result = { + locale: internals.locale, + numberingSystem: internals.numberingSystem, + style: internals.style, + }; + + // currency, currencyDisplay, and currencySign are only present for currency + // formatters. + assert( + hasOwn("currency", internals) === (internals.style === "currency"), + "currency is present iff style is 'currency'" + ); + assert( + hasOwn("currencyDisplay", internals) === (internals.style === "currency"), + "currencyDisplay is present iff style is 'currency'" + ); + assert( + hasOwn("currencySign", internals) === (internals.style === "currency"), + "currencySign is present iff style is 'currency'" + ); + + if (hasOwn("currency", internals)) { + DefineDataProperty(result, "currency", internals.currency); + DefineDataProperty(result, "currencyDisplay", internals.currencyDisplay); + DefineDataProperty(result, "currencySign", internals.currencySign); + } + + // unit and unitDisplay are only present for unit formatters. + assert( + hasOwn("unit", internals) === (internals.style === "unit"), + "unit is present iff style is 'unit'" + ); + assert( + hasOwn("unitDisplay", internals) === (internals.style === "unit"), + "unitDisplay is present iff style is 'unit'" + ); + + if (hasOwn("unit", internals)) { + DefineDataProperty(result, "unit", internals.unit); + DefineDataProperty(result, "unitDisplay", internals.unitDisplay); + } + + DefineDataProperty( + result, + "minimumIntegerDigits", + internals.minimumIntegerDigits + ); + + // Min/Max fraction digits are either both present or not present at all. + assert( + hasOwn("minimumFractionDigits", internals) === + hasOwn("maximumFractionDigits", internals), + "minimumFractionDigits is present iff maximumFractionDigits is present" + ); + + if (hasOwn("minimumFractionDigits", internals)) { + DefineDataProperty( + result, + "minimumFractionDigits", + internals.minimumFractionDigits + ); + DefineDataProperty( + result, + "maximumFractionDigits", + internals.maximumFractionDigits + ); + } + + // Min/Max significant digits are either both present or not present at all. + assert( + hasOwn("minimumSignificantDigits", internals) === + hasOwn("maximumSignificantDigits", internals), + "minimumSignificantDigits is present iff maximumSignificantDigits is present" + ); + + if (hasOwn("minimumSignificantDigits", internals)) { + DefineDataProperty( + result, + "minimumSignificantDigits", + internals.minimumSignificantDigits + ); + DefineDataProperty( + result, + "maximumSignificantDigits", + internals.maximumSignificantDigits + ); + } + + DefineDataProperty(result, "useGrouping", internals.useGrouping); + + var notation = internals.notation; + DefineDataProperty(result, "notation", notation); + + if (notation === "compact") { + DefineDataProperty(result, "compactDisplay", internals.compactDisplay); + } + + DefineDataProperty(result, "signDisplay", internals.signDisplay); + +#ifdef NIGHTLY_BUILD + DefineDataProperty(result, "roundingMode", internals.roundingMode); + DefineDataProperty(result, "roundingIncrement", internals.roundingIncrement); + DefineDataProperty( + result, + "trailingZeroDisplay", + internals.trailingZeroDisplay + ); + DefineDataProperty(result, "roundingPriority", internals.roundingPriority); +#endif + + // Step 6. + return result; +} diff --git a/js/src/builtin/intl/NumberingSystems.yaml b/js/src/builtin/intl/NumberingSystems.yaml new file mode 100644 index 0000000000..db287c10ef --- /dev/null +++ b/js/src/builtin/intl/NumberingSystems.yaml @@ -0,0 +1,82 @@ +# 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/. + +# 12.1.7 PartitionNotationSubPattern ( numberFormat, x, n, exponent ) +# +# Numbering systems with simple digit mappings +# +# https://tc39.es/ecma402/#table-numbering-system-digits + +# Run |make_intl_data numbering| to regenerate all files which reference this list +# of numbering systems. + +- adlm +- ahom +- arab +- arabext +- bali +- beng +- bhks +- brah +- cakm +- cham +- deva +- diak +- fullwide +- gong +- gonm +- gujr +- guru +- hanidec +- hmng +- hmnp +- java +- kali +- kawi +- khmr +- knda +- lana +- lanatham +- laoo +- latn +- lepc +- limb +- mathbold +- mathdbl +- mathmono +- mathsanb +- mathsans +- mlym +- modi +- mong +- mroo +- mtei +- mymr +- mymrshan +- mymrtlng +- nagm +- newa +- nkoo +- olck +- orya +- osma +- rohg +- saur +- segment +- shrd +- sind +- sinh +- sora +- sund +- takr +- talu +- tamldec +- telu +- thai +- tibt +- tirh +- tnsa +- vaii +- wara +- wcho diff --git a/js/src/builtin/intl/NumberingSystemsGenerated.h b/js/src/builtin/intl/NumberingSystemsGenerated.h new file mode 100644 index 0000000000..f51d0f9c53 --- /dev/null +++ b/js/src/builtin/intl/NumberingSystemsGenerated.h @@ -0,0 +1,83 @@ +// Generated by make_intl_data.py. DO NOT EDIT. + +/** + * The list of numbering systems with simple digit mappings. + */ + +#ifndef builtin_intl_NumberingSystemsGenerated_h +#define builtin_intl_NumberingSystemsGenerated_h + +// clang-format off +#define NUMBERING_SYSTEMS_WITH_SIMPLE_DIGIT_MAPPINGS \ + "adlm", \ + "ahom", \ + "arab", \ + "arabext", \ + "bali", \ + "beng", \ + "bhks", \ + "brah", \ + "cakm", \ + "cham", \ + "deva", \ + "diak", \ + "fullwide", \ + "gong", \ + "gonm", \ + "gujr", \ + "guru", \ + "hanidec", \ + "hmng", \ + "hmnp", \ + "java", \ + "kali", \ + "kawi", \ + "khmr", \ + "knda", \ + "lana", \ + "lanatham", \ + "laoo", \ + "latn", \ + "lepc", \ + "limb", \ + "mathbold", \ + "mathdbl", \ + "mathmono", \ + "mathsanb", \ + "mathsans", \ + "mlym", \ + "modi", \ + "mong", \ + "mroo", \ + "mtei", \ + "mymr", \ + "mymrshan", \ + "mymrtlng", \ + "nagm", \ + "newa", \ + "nkoo", \ + "olck", \ + "orya", \ + "osma", \ + "rohg", \ + "saur", \ + "segment", \ + "shrd", \ + "sind", \ + "sinh", \ + "sora", \ + "sund", \ + "takr", \ + "talu", \ + "tamldec", \ + "telu", \ + "thai", \ + "tibt", \ + "tirh", \ + "tnsa", \ + "vaii", \ + "wara", \ + "wcho" +// clang-format on + +#endif // builtin_intl_NumberingSystemsGenerated_h diff --git a/js/src/builtin/intl/PluralRules.cpp b/js/src/builtin/intl/PluralRules.cpp new file mode 100644 index 0000000000..480c181765 --- /dev/null +++ b/js/src/builtin/intl/PluralRules.cpp @@ -0,0 +1,423 @@ +/* -*- 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/. */ + +/* Implementation of the Intl.PluralRules proposal. */ + +#include "builtin/intl/PluralRules.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Casting.h" +#include "mozilla/intl/PluralRules.h" + +#include "builtin/Array.h" +#include "builtin/intl/CommonFunctions.h" +#include "gc/GCContext.h" +#include "js/PropertySpec.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/StringType.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::AssertedCast; + +const JSClassOps PluralRulesObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + PluralRulesObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass PluralRulesObject::class_ = { + "Intl.PluralRules", + JSCLASS_HAS_RESERVED_SLOTS(PluralRulesObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_PluralRules) | + JSCLASS_FOREGROUND_FINALIZE, + &PluralRulesObject::classOps_, &PluralRulesObject::classSpec_}; + +const JSClass& PluralRulesObject::protoClass_ = PlainObject::class_; + +static bool pluralRules_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().PluralRules); + return true; +} + +static const JSFunctionSpec pluralRules_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", + "Intl_PluralRules_supportedLocalesOf", 1, 0), + JS_FS_END}; + +static const JSFunctionSpec pluralRules_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_PluralRules_resolvedOptions", 0, + 0), + JS_SELF_HOSTED_FN("select", "Intl_PluralRules_select", 1, 0), +#ifdef NIGHTLY_BUILD + JS_SELF_HOSTED_FN("selectRange", "Intl_PluralRules_selectRange", 2, 0), +#endif + JS_FN(js_toSource_str, pluralRules_toSource, 0, 0), JS_FS_END}; + +static const JSPropertySpec pluralRules_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Intl.PluralRules", JSPROP_READONLY), + JS_PS_END}; + +static bool PluralRules(JSContext* cx, unsigned argc, Value* vp); + +const ClassSpec PluralRulesObject::classSpec_ = { + GenericCreateConstructor<PluralRules, 0, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<PluralRulesObject>, + pluralRules_static_methods, + nullptr, + pluralRules_methods, + pluralRules_properties, + nullptr, + ClassSpec::DontDefineConstructor}; + +/** + * PluralRules constructor. + * Spec: ECMAScript 402 API, PluralRules, 13.2.1 + */ +static bool PluralRules(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Intl.PluralRules")) { + return false; + } + + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_PluralRules, + &proto)) { + return false; + } + + Rooted<PluralRulesObject*> pluralRules(cx); + pluralRules = NewObjectWithClassProto<PluralRulesObject>(cx, proto); + if (!pluralRules) { + return false; + } + + HandleValue locales = args.get(0); + HandleValue options = args.get(1); + + // Step 3. + if (!intl::InitializeObject(cx, pluralRules, + cx->names().InitializePluralRules, locales, + options)) { + return false; + } + + args.rval().setObject(*pluralRules); + return true; +} + +void js::PluralRulesObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + + auto* pluralRules = &obj->as<PluralRulesObject>(); + if (mozilla::intl::PluralRules* pr = pluralRules->getPluralRules()) { + intl::RemoveICUCellMemory( + gcx, obj, PluralRulesObject::UPluralRulesEstimatedMemoryUse); + delete pr; + } +} + +static JSString* KeywordToString(mozilla::intl::PluralRules::Keyword keyword, + JSContext* cx) { + using Keyword = mozilla::intl::PluralRules::Keyword; + switch (keyword) { + case Keyword::Zero: { + return cx->names().zero; + } + case Keyword::One: { + return cx->names().one; + } + case Keyword::Two: { + return cx->names().two; + } + case Keyword::Few: { + return cx->names().few; + } + case Keyword::Many: { + return cx->names().many; + } + case Keyword::Other: { + return cx->names().other; + } + } + MOZ_CRASH("Unexpected PluralRules keyword"); +} + +/** + * Returns a new intl::PluralRules with the locale and type options of the given + * PluralRules. + */ +static mozilla::intl::PluralRules* NewPluralRules( + JSContext* cx, Handle<PluralRulesObject*> pluralRules) { + RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules)); + if (!internals) { + return nullptr; + } + + RootedValue value(cx); + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) { + return nullptr; + } + UniqueChars locale = intl::EncodeLocale(cx, value.toString()); + if (!locale) { + return nullptr; + } + + using PluralRules = mozilla::intl::PluralRules; + mozilla::intl::PluralRulesOptions options; + + if (!GetProperty(cx, internals, internals, cx->names().type, &value)) { + return nullptr; + } + + { + JSLinearString* type = value.toString()->ensureLinear(cx); + if (!type) { + return nullptr; + } + + if (StringEqualsLiteral(type, "ordinal")) { + options.mPluralType = PluralRules::Type::Ordinal; + } else { + MOZ_ASSERT(StringEqualsLiteral(type, "cardinal")); + options.mPluralType = PluralRules::Type::Cardinal; + } + } + + bool hasMinimumSignificantDigits; + if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits, + &hasMinimumSignificantDigits)) { + return nullptr; + } + + if (hasMinimumSignificantDigits) { + if (!GetProperty(cx, internals, internals, + cx->names().minimumSignificantDigits, &value)) { + return nullptr; + } + uint32_t minimumSignificantDigits = AssertedCast<uint32_t>(value.toInt32()); + + if (!GetProperty(cx, internals, internals, + cx->names().maximumSignificantDigits, &value)) { + return nullptr; + } + uint32_t maximumSignificantDigits = AssertedCast<uint32_t>(value.toInt32()); + + options.mSignificantDigits = mozilla::Some( + std::make_pair(minimumSignificantDigits, maximumSignificantDigits)); + } else { + if (!GetProperty(cx, internals, internals, + cx->names().minimumFractionDigits, &value)) { + return nullptr; + } + uint32_t minimumFractionDigits = AssertedCast<uint32_t>(value.toInt32()); + + if (!GetProperty(cx, internals, internals, + cx->names().maximumFractionDigits, &value)) { + return nullptr; + } + uint32_t maximumFractionDigits = AssertedCast<uint32_t>(value.toInt32()); + + options.mFractionDigits = mozilla::Some( + std::make_pair(minimumFractionDigits, maximumFractionDigits)); + } + + if (!GetProperty(cx, internals, internals, cx->names().roundingPriority, + &value)) { + return nullptr; + } + + { + JSLinearString* roundingPriority = value.toString()->ensureLinear(cx); + if (!roundingPriority) { + return nullptr; + } + + using RoundingPriority = + mozilla::intl::PluralRulesOptions::RoundingPriority; + + RoundingPriority priority; + if (StringEqualsLiteral(roundingPriority, "auto")) { + priority = RoundingPriority::Auto; + } else if (StringEqualsLiteral(roundingPriority, "morePrecision")) { + priority = RoundingPriority::MorePrecision; + } else { + MOZ_ASSERT(StringEqualsLiteral(roundingPriority, "lessPrecision")); + priority = RoundingPriority::LessPrecision; + } + + options.mRoundingPriority = priority; + } + + if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, + &value)) { + return nullptr; + } + options.mMinIntegerDigits = + mozilla::Some(AssertedCast<uint32_t>(value.toInt32())); + + auto result = PluralRules::TryCreate(locale.get(), options); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + + return result.unwrap().release(); +} + +static mozilla::intl::PluralRules* GetOrCreatePluralRules( + JSContext* cx, Handle<PluralRulesObject*> pluralRules) { + // Obtain a cached PluralRules object. + mozilla::intl::PluralRules* pr = pluralRules->getPluralRules(); + if (pr) { + return pr; + } + + pr = NewPluralRules(cx, pluralRules); + if (!pr) { + return nullptr; + } + pluralRules->setPluralRules(pr); + + intl::AddICUCellMemory(pluralRules, + PluralRulesObject::UPluralRulesEstimatedMemoryUse); + return pr; +} + +bool js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + + Rooted<PluralRulesObject*> pluralRules( + cx, &args[0].toObject().as<PluralRulesObject>()); + + double x = args[1].toNumber(); + + using PluralRules = mozilla::intl::PluralRules; + PluralRules* pr = GetOrCreatePluralRules(cx, pluralRules); + if (!pr) { + return false; + } + + auto keywordResult = pr->Select(x); + if (keywordResult.isErr()) { + intl::ReportInternalError(cx, keywordResult.unwrapErr()); + return false; + } + + JSString* str = KeywordToString(keywordResult.unwrap(), cx); + MOZ_ASSERT(str); + + args.rval().setString(str); + return true; +} + +/** + * ResolvePluralRange ( pluralRules, x, y ) + * PluralRuleSelectRange ( locale, type, xp, yp ) + */ +bool js::intl_SelectPluralRuleRange(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + + // Steps 1-2. + Rooted<PluralRulesObject*> pluralRules( + cx, &args[0].toObject().as<PluralRulesObject>()); + + // Steps 3-4. + double x = args[1].toNumber(); + double y = args[2].toNumber(); + + // Step 5. + if (std::isnan(x)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NAN_NUMBER_RANGE, "start", "PluralRules", + "selectRange"); + return false; + } + if (std::isnan(y)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_NAN_NUMBER_RANGE, "end", "PluralRules", + "selectRange"); + return false; + } + + using PluralRules = mozilla::intl::PluralRules; + PluralRules* pr = GetOrCreatePluralRules(cx, pluralRules); + if (!pr) { + return false; + } + + // Steps 6-10. + auto keywordResult = pr->SelectRange(x, y); + if (keywordResult.isErr()) { + intl::ReportInternalError(cx, keywordResult.unwrapErr()); + return false; + } + + JSString* str = KeywordToString(keywordResult.unwrap(), cx); + MOZ_ASSERT(str); + + args.rval().setString(str); + return true; +} + +bool js::intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + Rooted<PluralRulesObject*> pluralRules( + cx, &args[0].toObject().as<PluralRulesObject>()); + + using PluralRules = mozilla::intl::PluralRules; + PluralRules* pr = GetOrCreatePluralRules(cx, pluralRules); + if (!pr) { + return false; + } + + auto categoriesResult = pr->Categories(); + if (categoriesResult.isErr()) { + intl::ReportInternalError(cx, categoriesResult.unwrapErr()); + return false; + } + auto categories = categoriesResult.unwrap(); + + ArrayObject* res = NewDenseFullyAllocatedArray(cx, categories.size()); + if (!res) { + return false; + } + res->setDenseInitializedLength(categories.size()); + + size_t index = 0; + for (PluralRules::Keyword keyword : categories) { + JSString* str = KeywordToString(keyword, cx); + MOZ_ASSERT(str); + + res->initDenseElement(index++, StringValue(str)); + } + MOZ_ASSERT(index == categories.size()); + + args.rval().setObject(*res); + return true; +} diff --git a/js/src/builtin/intl/PluralRules.h b/js/src/builtin/intl/PluralRules.h new file mode 100644 index 0000000000..86d8ec105d --- /dev/null +++ b/js/src/builtin/intl/PluralRules.h @@ -0,0 +1,98 @@ +/* -*- 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 builtin_intl_PluralRules_h +#define builtin_intl_PluralRules_h + +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "vm/NativeObject.h" + +namespace mozilla::intl { +class PluralRules; +} + +namespace js { + +class PluralRulesObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t PLURAL_RULES_SLOT = 1; + static constexpr uint32_t SLOT_COUNT = 2; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals " + "object slot"); + + // Estimated memory use for UPluralRules (see IcuMemoryUsage). + // Includes usage for UNumberFormat and UNumberRangeFormatter since our + // PluralRules implementations contains a NumberFormat and a NumberRangeFormat + // object. + static constexpr size_t UPluralRulesEstimatedMemoryUse = 5736; + + mozilla::intl::PluralRules* getPluralRules() const { + const auto& slot = getFixedSlot(PLURAL_RULES_SLOT); + if (slot.isUndefined()) { + return nullptr; + } + return static_cast<mozilla::intl::PluralRules*>(slot.toPrivate()); + } + + void setPluralRules(mozilla::intl::PluralRules* pluralRules) { + setFixedSlot(PLURAL_RULES_SLOT, PrivateValue(pluralRules)); + } + + private: + static const JSClassOps classOps_; + static const ClassSpec classSpec_; + + static void finalize(JS::GCContext* gcx, JSObject* obj); +}; + +/** + * Returns a plural rule for the number x according to the effective + * locale and the formatting options of the given PluralRules. + * + * A plural rule is a grammatical category that expresses count distinctions + * (such as "one", "two", "few" etc.). + * + * Usage: rule = intl_SelectPluralRule(pluralRules, x) + */ +[[nodiscard]] extern bool intl_SelectPluralRule(JSContext* cx, unsigned argc, + JS::Value* vp); + +/** + * Returns a plural rule for the number range «x - y» according to the effective + * locale and the formatting options of the given PluralRules. + * + * A plural rule is a grammatical category that expresses count distinctions + * (such as "one", "two", "few" etc.). + * + * Usage: rule = intl_SelectPluralRuleRange(pluralRules, x, y) + */ +[[nodiscard]] extern bool intl_SelectPluralRuleRange(JSContext* cx, + unsigned argc, + JS::Value* vp); + +/** + * Returns an array of plural rules categories for a given pluralRules object. + * + * Usage: categories = intl_GetPluralCategories(pluralRules) + * + * Example: + * + * pluralRules = new Intl.PluralRules('pl', {type: 'cardinal'}); + * intl_getPluralCategories(pluralRules); // ['one', 'few', 'many', 'other'] + */ +[[nodiscard]] extern bool intl_GetPluralCategories(JSContext* cx, unsigned argc, + JS::Value* vp); + +} // namespace js + +#endif /* builtin_intl_PluralRules_h */ diff --git a/js/src/builtin/intl/PluralRules.js b/js/src/builtin/intl/PluralRules.js new file mode 100644 index 0000000000..a90fc32d0b --- /dev/null +++ b/js/src/builtin/intl/PluralRules.js @@ -0,0 +1,390 @@ +/* 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/. */ + +/** + * PluralRules internal properties. + * + * Spec: ECMAScript 402 API, PluralRules, 13.3.3. + */ +var pluralRulesInternalProperties = { + localeData: pluralRulesLocaleData, + relevantExtensionKeys: [], +}; + +function pluralRulesLocaleData() { + // PluralRules don't support any extension keys. + return {}; +} + +/** + * Compute an internal properties object from |lazyPluralRulesData|. + */ +function resolvePluralRulesInternals(lazyPluralRulesData) { + assert(IsObject(lazyPluralRulesData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + var PluralRules = pluralRulesInternalProperties; + + // Compute effective locale. + + // Step 10. + var localeData = PluralRules.localeData; + + // Step 11. + const r = ResolveLocale( + "PluralRules", + lazyPluralRulesData.requestedLocales, + lazyPluralRulesData.opt, + PluralRules.relevantExtensionKeys, + localeData + ); + + // Step 12. + internalProps.locale = r.locale; + + // Step 8. + internalProps.type = lazyPluralRulesData.type; + + // Step 9. + internalProps.minimumIntegerDigits = lazyPluralRulesData.minimumIntegerDigits; + + if ("minimumFractionDigits" in lazyPluralRulesData) { + assert( + "maximumFractionDigits" in lazyPluralRulesData, + "min/max frac digits mismatch" + ); + internalProps.minimumFractionDigits = + lazyPluralRulesData.minimumFractionDigits; + internalProps.maximumFractionDigits = + lazyPluralRulesData.maximumFractionDigits; + } + + if ("minimumSignificantDigits" in lazyPluralRulesData) { + assert( + "maximumSignificantDigits" in lazyPluralRulesData, + "min/max sig digits mismatch" + ); + internalProps.minimumSignificantDigits = + lazyPluralRulesData.minimumSignificantDigits; + internalProps.maximumSignificantDigits = + lazyPluralRulesData.maximumSignificantDigits; + } + + // Intl.NumberFormat v3 Proposal + internalProps.roundingPriority = lazyPluralRulesData.roundingPriority; + + // Step 13 (lazily computed on first access). + internalProps.pluralCategories = null; + + return internalProps; +} + +/** + * Returns an object containing the PluralRules internal properties of |obj|. + */ +function getPluralRulesInternals(obj) { + assert(IsObject(obj), "getPluralRulesInternals called with non-object"); + assert( + intl_GuardToPluralRules(obj) !== null, + "getPluralRulesInternals called with non-PluralRules" + ); + + var internals = getIntlObjectInternals(obj); + assert( + internals.type === "PluralRules", + "bad type escaped getIntlObjectInternals" + ); + + var internalProps = maybeInternalProperties(internals); + if (internalProps) { + return internalProps; + } + + internalProps = resolvePluralRulesInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * Initializes an object as a PluralRules. + * + * This method is complicated a moderate bit by its implementing initialization + * as a *lazy* concept. Everything that must happen now, does -- but we defer + * all the work we can until the object is actually used as a PluralRules. + * This later work occurs in |resolvePluralRulesInternals|; steps not noted + * here occur there. + * + * Spec: ECMAScript 402 API, PluralRules, 13.1.1. + */ +function InitializePluralRules(pluralRules, locales, options) { + assert(IsObject(pluralRules), "InitializePluralRules called with non-object"); + assert( + intl_GuardToPluralRules(pluralRules) !== null, + "InitializePluralRules called with non-PluralRules" + ); + + // Lazy PluralRules data has the following structure: + // + // { + // requestedLocales: List of locales, + // type: "cardinal" / "ordinal", + // + // opt: // opt object computer in InitializePluralRules + // { + // localeMatcher: "lookup" / "best fit", + // } + // + // minimumIntegerDigits: integer ∈ [1, 21], + // + // // optional, mutually exclusive with the significant-digits option + // minimumFractionDigits: integer ∈ [0, 20], + // maximumFractionDigits: integer ∈ [0, 20], + // + // // optional, mutually exclusive with the fraction-digits option + // minimumSignificantDigits: integer ∈ [1, 21], + // maximumSignificantDigits: integer ∈ [1, 21], + // + // roundingPriority: "auto" / "lessPrecision" / "morePrecision", + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every PluralRules lazy data object has *all* these properties, never a + // subset of them. + const lazyPluralRulesData = std_Object_create(null); + + // Step 1. + let requestedLocales = CanonicalizeLocaleList(locales); + lazyPluralRulesData.requestedLocales = requestedLocales; + + // Steps 2-3. + if (options === undefined) { + options = std_Object_create(null); + } else { + options = ToObject(options); + } + + // Step 4. + let opt = new_Record(); + lazyPluralRulesData.opt = opt; + + // Steps 5-6. + let matcher = GetOption( + options, + "localeMatcher", + "string", + ["lookup", "best fit"], + "best fit" + ); + opt.localeMatcher = matcher; + + // Step 7. + const type = GetOption( + options, + "type", + "string", + ["cardinal", "ordinal"], + "cardinal" + ); + lazyPluralRulesData.type = type; + + // Step 9. + SetNumberFormatDigitOptions(lazyPluralRulesData, options, 0, 3, "standard"); + + // Step 15. + // + // We've done everything that must be done now: mark the lazy data as fully + // computed and install it. + initializeIntlObject(pluralRules, "PluralRules", lazyPluralRulesData); +} + +/** + * Returns the subset of the given locale list for which this locale list has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript 402 API, PluralRules, 13.3.2. + */ +function Intl_PluralRules_supportedLocalesOf(locales /*, options*/) { + var options = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 1. + var availableLocales = "PluralRules"; + + // Step 2. + let requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. + return SupportedLocales(availableLocales, requestedLocales, options); +} + +/** + * Returns a String value representing the plural category matching + * the number passed as value according to the + * effective locale and the formatting options of this PluralRules. + * + * Spec: ECMAScript 402 API, PluralRules, 13.4.3. + */ +function Intl_PluralRules_select(value) { + // Step 1. + let pluralRules = this; + + // Steps 2-3. + if ( + !IsObject(pluralRules) || + (pluralRules = intl_GuardToPluralRules(pluralRules)) === null + ) { + return callFunction( + intl_CallPluralRulesMethodIfWrapped, + this, + value, + "Intl_PluralRules_select" + ); + } + + // Step 4. + let n = ToNumber(value); + + // Ensure the PluralRules internals are resolved. + getPluralRulesInternals(pluralRules); + + // Step 5. + return intl_SelectPluralRule(pluralRules, n); +} + +/** + * Returns a String value representing the plural category matching the input + * number range according to the effective locale and the formatting options + * of this PluralRules. + */ +function Intl_PluralRules_selectRange(start, end) { + // Step 1. + var pluralRules = this; + + // Step 2. + if ( + !IsObject(pluralRules) || + (pluralRules = intl_GuardToPluralRules(pluralRules)) === null + ) { + return callFunction( + intl_CallPluralRulesMethodIfWrapped, + this, + start, + end, + "Intl_PluralRules_selectRange" + ); + } + + // Step 3. + if (start === undefined || end === undefined) { + ThrowTypeError( + JSMSG_UNDEFINED_NUMBER, + start === undefined ? "start" : "end", + "PluralRules", + "selectRange" + ); + } + + // Step 4. + var x = ToNumber(start); + + // Step 5. + var y = ToNumber(end); + + // Step 6. + return intl_SelectPluralRuleRange(pluralRules, x, y); +} + +/** + * Returns the resolved options for a PluralRules object. + * + * Spec: ECMAScript 402 API, PluralRules, 13.4.4. + */ +function Intl_PluralRules_resolvedOptions() { + // Step 1. + var pluralRules = this; + + // Steps 2-3. + if ( + !IsObject(pluralRules) || + (pluralRules = intl_GuardToPluralRules(pluralRules)) === null + ) { + return callFunction( + intl_CallPluralRulesMethodIfWrapped, + this, + "Intl_PluralRules_resolvedOptions" + ); + } + + var internals = getPluralRulesInternals(pluralRules); + + // Steps 4-5. + var result = { + locale: internals.locale, + type: internals.type, + minimumIntegerDigits: internals.minimumIntegerDigits, + }; + + // Min/Max fraction digits are either both present or not present at all. + assert( + hasOwn("minimumFractionDigits", internals) === + hasOwn("maximumFractionDigits", internals), + "minimumFractionDigits is present iff maximumFractionDigits is present" + ); + + if (hasOwn("minimumFractionDigits", internals)) { + DefineDataProperty( + result, + "minimumFractionDigits", + internals.minimumFractionDigits + ); + DefineDataProperty( + result, + "maximumFractionDigits", + internals.maximumFractionDigits + ); + } + + // Min/Max significant digits are either both present or not present at all. + assert( + hasOwn("minimumSignificantDigits", internals) === + hasOwn("maximumSignificantDigits", internals), + "minimumSignificantDigits is present iff maximumSignificantDigits is present" + ); + + if (hasOwn("minimumSignificantDigits", internals)) { + DefineDataProperty( + result, + "minimumSignificantDigits", + internals.minimumSignificantDigits + ); + DefineDataProperty( + result, + "maximumSignificantDigits", + internals.maximumSignificantDigits + ); + } + + // Step 6. + var internalsPluralCategories = internals.pluralCategories; + if (internalsPluralCategories === null) { + internalsPluralCategories = intl_GetPluralCategories(pluralRules); + internals.pluralCategories = internalsPluralCategories; + } + + var pluralCategories = []; + for (var i = 0; i < internalsPluralCategories.length; i++) { + DefineDataProperty(pluralCategories, i, internalsPluralCategories[i]); + } + + // Step 7. + DefineDataProperty(result, "pluralCategories", pluralCategories); + +#ifdef NIGHTLY_BUILD + DefineDataProperty(result, "roundingPriority", internals.roundingPriority); +#endif + + // Step 8. + return result; +} diff --git a/js/src/builtin/intl/RelativeTimeFormat.cpp b/js/src/builtin/intl/RelativeTimeFormat.cpp new file mode 100644 index 0000000000..11f997f1c6 --- /dev/null +++ b/js/src/builtin/intl/RelativeTimeFormat.cpp @@ -0,0 +1,403 @@ +/* -*- 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/. */ + +/* Implementation of the Intl.RelativeTimeFormat proposal. */ + +#include "builtin/intl/RelativeTimeFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/intl/RelativeTimeFormat.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/FormatBuffer.h" +#include "builtin/intl/LanguageTag.h" +#include "gc/GCContext.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/Printer.h" +#include "js/PropertySpec.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/StringType.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/NativeObject-inl.h" + +using namespace js; + +/**************** RelativeTimeFormat *****************/ + +const JSClassOps RelativeTimeFormatObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + RelativeTimeFormatObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass RelativeTimeFormatObject::class_ = { + "Intl.RelativeTimeFormat", + JSCLASS_HAS_RESERVED_SLOTS(RelativeTimeFormatObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_RelativeTimeFormat) | + JSCLASS_FOREGROUND_FINALIZE, + &RelativeTimeFormatObject::classOps_, + &RelativeTimeFormatObject::classSpec_}; + +const JSClass& RelativeTimeFormatObject::protoClass_ = PlainObject::class_; + +static bool relativeTimeFormat_toSource(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().RelativeTimeFormat); + return true; +} + +static const JSFunctionSpec relativeTimeFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", + "Intl_RelativeTimeFormat_supportedLocalesOf", 1, 0), + JS_FS_END}; + +static const JSFunctionSpec relativeTimeFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", + "Intl_RelativeTimeFormat_resolvedOptions", 0, 0), + JS_SELF_HOSTED_FN("format", "Intl_RelativeTimeFormat_format", 2, 0), + JS_SELF_HOSTED_FN("formatToParts", "Intl_RelativeTimeFormat_formatToParts", + 2, 0), + JS_FN(js_toSource_str, relativeTimeFormat_toSource, 0, 0), JS_FS_END}; + +static const JSPropertySpec relativeTimeFormat_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Intl.RelativeTimeFormat", JSPROP_READONLY), + JS_PS_END}; + +static bool RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp); + +const ClassSpec RelativeTimeFormatObject::classSpec_ = { + GenericCreateConstructor<RelativeTimeFormat, 0, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<RelativeTimeFormatObject>, + relativeTimeFormat_static_methods, + nullptr, + relativeTimeFormat_methods, + relativeTimeFormat_properties, + nullptr, + ClassSpec::DontDefineConstructor}; + +/** + * RelativeTimeFormat constructor. + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1 + */ +static bool RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Intl.RelativeTimeFormat")) { + return false; + } + + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_RelativeTimeFormat, + &proto)) { + return false; + } + + Rooted<RelativeTimeFormatObject*> relativeTimeFormat(cx); + relativeTimeFormat = + NewObjectWithClassProto<RelativeTimeFormatObject>(cx, proto); + if (!relativeTimeFormat) { + return false; + } + + HandleValue locales = args.get(0); + HandleValue options = args.get(1); + + // Step 3. + if (!intl::InitializeObject(cx, relativeTimeFormat, + cx->names().InitializeRelativeTimeFormat, locales, + options)) { + return false; + } + + args.rval().setObject(*relativeTimeFormat); + return true; +} + +void js::RelativeTimeFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + + if (mozilla::intl::RelativeTimeFormat* rtf = + obj->as<RelativeTimeFormatObject>().getRelativeTimeFormatter()) { + intl::RemoveICUCellMemory(gcx, obj, + RelativeTimeFormatObject::EstimatedMemoryUse); + + // This was allocated using `new` in mozilla::intl::RelativeTimeFormat, + // so we delete here. + delete rtf; + } +} + +/** + * Returns a new URelativeDateTimeFormatter with the locale and options of the + * given RelativeTimeFormatObject. + */ +static mozilla::intl::RelativeTimeFormat* NewRelativeTimeFormatter( + JSContext* cx, Handle<RelativeTimeFormatObject*> relativeTimeFormat) { + RootedObject internals(cx, intl::GetInternalsObject(cx, relativeTimeFormat)); + if (!internals) { + return nullptr; + } + + RootedValue value(cx); + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) { + return nullptr; + } + + // ICU expects numberingSystem as a Unicode locale extensions on locale. + + mozilla::intl::Locale tag; + { + Rooted<JSLinearString*> locale(cx, value.toString()->ensureLinear(cx)); + if (!locale) { + return nullptr; + } + + if (!intl::ParseLocale(cx, locale, tag)) { + return nullptr; + } + } + + JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx); + + if (!GetProperty(cx, internals, internals, cx->names().numberingSystem, + &value)) { + return nullptr; + } + + { + JSLinearString* numberingSystem = value.toString()->ensureLinear(cx); + if (!numberingSystem) { + return nullptr; + } + + if (!keywords.emplaceBack("nu", numberingSystem)) { + return nullptr; + } + } + + // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of the + // Unicode extension subtag. We're then relying on ICU to follow RFC 6067, + // which states that any trailing keywords using the same key should be + // ignored. + if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) { + return nullptr; + } + + intl::FormatBuffer<char> buffer(cx); + if (auto result = tag.ToString(buffer); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + + UniqueChars locale = buffer.extractStringZ(); + if (!locale) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().style, &value)) { + return nullptr; + } + + using RelativeTimeFormatOptions = mozilla::intl::RelativeTimeFormatOptions; + RelativeTimeFormatOptions options; + { + JSLinearString* style = value.toString()->ensureLinear(cx); + if (!style) { + return nullptr; + } + + if (StringEqualsLiteral(style, "short")) { + options.style = RelativeTimeFormatOptions::Style::Short; + } else if (StringEqualsLiteral(style, "narrow")) { + options.style = RelativeTimeFormatOptions::Style::Narrow; + } else { + MOZ_ASSERT(StringEqualsLiteral(style, "long")); + options.style = RelativeTimeFormatOptions::Style::Long; + } + } + + if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) { + return nullptr; + } + + { + JSLinearString* numeric = value.toString()->ensureLinear(cx); + if (!numeric) { + return nullptr; + } + + if (StringEqualsLiteral(numeric, "auto")) { + options.numeric = RelativeTimeFormatOptions::Numeric::Auto; + } else { + MOZ_ASSERT(StringEqualsLiteral(numeric, "always")); + options.numeric = RelativeTimeFormatOptions::Numeric::Always; + } + } + + using RelativeTimeFormat = mozilla::intl::RelativeTimeFormat; + mozilla::Result<mozilla::UniquePtr<RelativeTimeFormat>, + mozilla::intl::ICUError> + result = RelativeTimeFormat::TryCreate(locale.get(), options); + + if (result.isOk()) { + return result.unwrap().release(); + } + + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; +} + +static mozilla::intl::RelativeTimeFormat* GetOrCreateRelativeTimeFormat( + JSContext* cx, Handle<RelativeTimeFormatObject*> relativeTimeFormat) { + // Obtain a cached RelativeDateTimeFormatter object. + mozilla::intl::RelativeTimeFormat* rtf = + relativeTimeFormat->getRelativeTimeFormatter(); + if (rtf) { + return rtf; + } + + rtf = NewRelativeTimeFormatter(cx, relativeTimeFormat); + if (!rtf) { + return nullptr; + } + relativeTimeFormat->setRelativeTimeFormatter(rtf); + + intl::AddICUCellMemory(relativeTimeFormat, + RelativeTimeFormatObject::EstimatedMemoryUse); + return rtf; +} + +bool js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 4); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(args[1].isNumber()); + MOZ_ASSERT(args[2].isString()); + MOZ_ASSERT(args[3].isBoolean()); + + Rooted<RelativeTimeFormatObject*> relativeTimeFormat(cx); + relativeTimeFormat = &args[0].toObject().as<RelativeTimeFormatObject>(); + + bool formatToParts = args[3].toBoolean(); + + // PartitionRelativeTimePattern, step 4. + double t = args[1].toNumber(); + if (!std::isfinite(t)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DATE_NOT_FINITE, "RelativeTimeFormat", + formatToParts ? "formatToParts" : "format"); + return false; + } + + mozilla::intl::RelativeTimeFormat* rtf = + GetOrCreateRelativeTimeFormat(cx, relativeTimeFormat); + if (!rtf) { + return false; + } + + intl::FieldType jsUnitType; + using FormatUnit = mozilla::intl::RelativeTimeFormat::FormatUnit; + FormatUnit relTimeUnit; + { + JSLinearString* unit = args[2].toString()->ensureLinear(cx); + if (!unit) { + return false; + } + + // PartitionRelativeTimePattern, step 5. + if (StringEqualsLiteral(unit, "second") || + StringEqualsLiteral(unit, "seconds")) { + jsUnitType = &JSAtomState::second; + relTimeUnit = FormatUnit::Second; + } else if (StringEqualsLiteral(unit, "minute") || + StringEqualsLiteral(unit, "minutes")) { + jsUnitType = &JSAtomState::minute; + relTimeUnit = FormatUnit::Minute; + } else if (StringEqualsLiteral(unit, "hour") || + StringEqualsLiteral(unit, "hours")) { + jsUnitType = &JSAtomState::hour; + relTimeUnit = FormatUnit::Hour; + } else if (StringEqualsLiteral(unit, "day") || + StringEqualsLiteral(unit, "days")) { + jsUnitType = &JSAtomState::day; + relTimeUnit = FormatUnit::Day; + } else if (StringEqualsLiteral(unit, "week") || + StringEqualsLiteral(unit, "weeks")) { + jsUnitType = &JSAtomState::week; + relTimeUnit = FormatUnit::Week; + } else if (StringEqualsLiteral(unit, "month") || + StringEqualsLiteral(unit, "months")) { + jsUnitType = &JSAtomState::month; + relTimeUnit = FormatUnit::Month; + } else if (StringEqualsLiteral(unit, "quarter") || + StringEqualsLiteral(unit, "quarters")) { + jsUnitType = &JSAtomState::quarter; + relTimeUnit = FormatUnit::Quarter; + } else if (StringEqualsLiteral(unit, "year") || + StringEqualsLiteral(unit, "years")) { + jsUnitType = &JSAtomState::year; + relTimeUnit = FormatUnit::Year; + } else { + if (auto unitChars = QuoteString(cx, unit, '"')) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "unit", + unitChars.get()); + } + return false; + } + } + + using ICUError = mozilla::intl::ICUError; + if (formatToParts) { + mozilla::intl::NumberPartVector parts; + mozilla::Result<mozilla::Span<const char16_t>, ICUError> result = + rtf->formatToParts(t, relTimeUnit, parts); + + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + RootedString str(cx, NewStringCopy<CanGC>(cx, result.unwrap())); + if (!str) { + return false; + } + + return js::intl::FormattedRelativeTimeToParts(cx, str, parts, jsUnitType, + args.rval()); + } + + js::intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + mozilla::Result<Ok, ICUError> result = rtf->format(t, relTimeUnit, buffer); + + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + JSString* str = buffer.toString(cx); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} diff --git a/js/src/builtin/intl/RelativeTimeFormat.h b/js/src/builtin/intl/RelativeTimeFormat.h new file mode 100644 index 0000000000..079f8d572c --- /dev/null +++ b/js/src/builtin/intl/RelativeTimeFormat.h @@ -0,0 +1,87 @@ +/* -*- 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 builtin_intl_RelativeTimeFormat_h +#define builtin_intl_RelativeTimeFormat_h + +#include "mozilla/intl/NumberPart.h" + +#include <stdint.h> + +#include "builtin/SelfHostingDefines.h" +#include "gc/Barrier.h" +#include "js/Class.h" +#include "vm/NativeObject.h" + +namespace mozilla::intl { +class RelativeTimeFormat; +} + +namespace js { + +class RelativeTimeFormatObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t URELATIVE_TIME_FORMAT_SLOT = 1; + static constexpr uint32_t SLOT_COUNT = 2; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals " + "object slot"); + + // Estimated memory use for URelativeDateTimeFormatter (see IcuMemoryUsage). + static constexpr size_t EstimatedMemoryUse = 8188; + + mozilla::intl::RelativeTimeFormat* getRelativeTimeFormatter() const { + const auto& slot = getFixedSlot(URELATIVE_TIME_FORMAT_SLOT); + if (slot.isUndefined()) { + return nullptr; + } + return static_cast<mozilla::intl::RelativeTimeFormat*>(slot.toPrivate()); + } + + void setRelativeTimeFormatter(mozilla::intl::RelativeTimeFormat* rtf) { + setFixedSlot(URELATIVE_TIME_FORMAT_SLOT, PrivateValue(rtf)); + } + + private: + static const JSClassOps classOps_; + static const ClassSpec classSpec_; + + static void finalize(JS::GCContext* gcx, JSObject* obj); +}; + +/** + * Returns a relative time as a string formatted according to the effective + * locale and the formatting options of the given RelativeTimeFormat. + * + * |t| should be a number representing a number to be formatted. + * |unit| should be "second", "minute", "hour", "day", "week", "month", + * "quarter", or "year". + * |numeric| should be "always" or "auto". + * + * Usage: formatted = intl_FormatRelativeTime(relativeTimeFormat, t, + * unit, numeric, formatToParts) + */ +[[nodiscard]] extern bool intl_FormatRelativeTime(JSContext* cx, unsigned argc, + JS::Value* vp); + +namespace intl { + +using FieldType = js::ImmutableTenuredPtr<PropertyName*> JSAtomState::*; + +[[nodiscard]] bool FormattedRelativeTimeToParts( + JSContext* cx, HandleString str, + const mozilla::intl::NumberPartVector& parts, FieldType relativeTimeUnit, + MutableHandleValue result); + +} // namespace intl +} // namespace js + +#endif /* builtin_intl_RelativeTimeFormat_h */ diff --git a/js/src/builtin/intl/RelativeTimeFormat.js b/js/src/builtin/intl/RelativeTimeFormat.js new file mode 100644 index 0000000000..ab1b126a96 --- /dev/null +++ b/js/src/builtin/intl/RelativeTimeFormat.js @@ -0,0 +1,329 @@ +/* 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/. */ + +/** + * RelativeTimeFormat internal properties. + * + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.3.3. + */ +var relativeTimeFormatInternalProperties = { + localeData: relativeTimeFormatLocaleData, + relevantExtensionKeys: ["nu"], +}; + +function relativeTimeFormatLocaleData() { + return { + nu: getNumberingSystems, + default: { + nu: intl_numberingSystem, + }, + }; +} + +/** + * Compute an internal properties object from |lazyRelativeTimeFormatData|. + */ +function resolveRelativeTimeFormatInternals(lazyRelativeTimeFormatData) { + assert(IsObject(lazyRelativeTimeFormatData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + var RelativeTimeFormat = relativeTimeFormatInternalProperties; + + // Steps 10-11. + const r = ResolveLocale( + "RelativeTimeFormat", + lazyRelativeTimeFormatData.requestedLocales, + lazyRelativeTimeFormatData.opt, + RelativeTimeFormat.relevantExtensionKeys, + RelativeTimeFormat.localeData + ); + + // Steps 12-13. + internalProps.locale = r.locale; + + // Step 14. + internalProps.numberingSystem = r.nu; + + // Step 15 (Not relevant in our implementation). + + // Step 17. + internalProps.style = lazyRelativeTimeFormatData.style; + + // Step 19. + internalProps.numeric = lazyRelativeTimeFormatData.numeric; + + // Steps 20-24 (Not relevant in our implementation). + + return internalProps; +} + +/** + * Returns an object containing the RelativeTimeFormat internal properties of |obj|. + */ +function getRelativeTimeFormatInternals(obj) { + assert( + IsObject(obj), + "getRelativeTimeFormatInternals called with non-object" + ); + assert( + intl_GuardToRelativeTimeFormat(obj) !== null, + "getRelativeTimeFormatInternals called with non-RelativeTimeFormat" + ); + + var internals = getIntlObjectInternals(obj); + assert( + internals.type === "RelativeTimeFormat", + "bad type escaped getIntlObjectInternals" + ); + + var internalProps = maybeInternalProperties(internals); + if (internalProps) { + return internalProps; + } + + internalProps = resolveRelativeTimeFormatInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * Initializes an object as a RelativeTimeFormat. + * + * This method is complicated a moderate bit by its implementing initialization + * as a *lazy* concept. Everything that must happen now, does -- but we defer + * all the work we can until the object is actually used as a RelativeTimeFormat. + * This later work occurs in |resolveRelativeTimeFormatInternals|; steps not noted + * here occur there. + * + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1.1. + */ +function InitializeRelativeTimeFormat(relativeTimeFormat, locales, options) { + assert( + IsObject(relativeTimeFormat), + "InitializeRelativeimeFormat called with non-object" + ); + assert( + intl_GuardToRelativeTimeFormat(relativeTimeFormat) !== null, + "InitializeRelativeTimeFormat called with non-RelativeTimeFormat" + ); + + // Lazy RelativeTimeFormat data has the following structure: + // + // { + // requestedLocales: List of locales, + // style: "long" / "short" / "narrow", + // numeric: "always" / "auto", + // + // opt: // opt object computed in InitializeRelativeTimeFormat + // { + // localeMatcher: "lookup" / "best fit", + // } + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every RelativeTimeFormat lazy data object has *all* these properties, never a + // subset of them. + const lazyRelativeTimeFormatData = std_Object_create(null); + + // Step 1. + let requestedLocales = CanonicalizeLocaleList(locales); + lazyRelativeTimeFormatData.requestedLocales = requestedLocales; + + // Steps 2-3. + if (options === undefined) { + options = std_Object_create(null); + } else { + options = ToObject(options); + } + + // Step 4. + let opt = new_Record(); + + // Steps 5-6. + let matcher = GetOption( + options, + "localeMatcher", + "string", + ["lookup", "best fit"], + "best fit" + ); + opt.localeMatcher = matcher; + + // Steps 7-9. + let numberingSystem = GetOption( + options, + "numberingSystem", + "string", + undefined, + undefined + ); + if (numberingSystem !== undefined) { + numberingSystem = intl_ValidateAndCanonicalizeUnicodeExtensionType( + numberingSystem, + "numberingSystem", + "nu" + ); + } + opt.nu = numberingSystem; + + lazyRelativeTimeFormatData.opt = opt; + + // Steps 16-17. + const style = GetOption( + options, + "style", + "string", + ["long", "short", "narrow"], + "long" + ); + lazyRelativeTimeFormatData.style = style; + + // Steps 18-19. + const numeric = GetOption( + options, + "numeric", + "string", + ["always", "auto"], + "always" + ); + lazyRelativeTimeFormatData.numeric = numeric; + + initializeIntlObject( + relativeTimeFormat, + "RelativeTimeFormat", + lazyRelativeTimeFormatData + ); +} + +/** + * Returns the subset of the given locale list for which this locale list has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.3.2. + */ +function Intl_RelativeTimeFormat_supportedLocalesOf(locales /*, options*/) { + var options = ArgumentsLength() > 1 ? GetArgument(1) : undefined; + + // Step 1. + var availableLocales = "RelativeTimeFormat"; + + // Step 2. + let requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. + return SupportedLocales(availableLocales, requestedLocales, options); +} + +/** + * Returns a String value representing the written form of a relative date + * formatted according to the effective locale and the formatting options + * of this RelativeTimeFormat object. + * + * Spec: ECMAScript 402 API, RelativeTImeFormat, 1.4.3. + */ +function Intl_RelativeTimeFormat_format(value, unit) { + // Step 1. + let relativeTimeFormat = this; + + // Step 2. + if ( + !IsObject(relativeTimeFormat) || + (relativeTimeFormat = intl_GuardToRelativeTimeFormat( + relativeTimeFormat + )) === null + ) { + return callFunction( + intl_CallRelativeTimeFormatMethodIfWrapped, + this, + value, + unit, + "Intl_RelativeTimeFormat_format" + ); + } + + // Step 3. + let t = ToNumber(value); + + // Step 4. + let u = ToString(unit); + + // Step 5. + return intl_FormatRelativeTime(relativeTimeFormat, t, u, false); +} + +/** + * Returns an Array composed of the components of a relative date formatted + * according to the effective locale and the formatting options of this + * RelativeTimeFormat object. + * + * Spec: ECMAScript 402 API, RelativeTImeFormat, 1.4.4. + */ +function Intl_RelativeTimeFormat_formatToParts(value, unit) { + // Step 1. + let relativeTimeFormat = this; + + // Step 2. + if ( + !IsObject(relativeTimeFormat) || + (relativeTimeFormat = intl_GuardToRelativeTimeFormat( + relativeTimeFormat + )) === null + ) { + return callFunction( + intl_CallRelativeTimeFormatMethodIfWrapped, + this, + value, + unit, + "Intl_RelativeTimeFormat_formatToParts" + ); + } + + // Step 3. + let t = ToNumber(value); + + // Step 4. + let u = ToString(unit); + + // Step 5. + return intl_FormatRelativeTime(relativeTimeFormat, t, u, true); +} + +/** + * Returns the resolved options for a RelativeTimeFormat object. + * + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.4.5. + */ +function Intl_RelativeTimeFormat_resolvedOptions() { + // Step 1. + var relativeTimeFormat = this; + + // Steps 2-3. + if ( + !IsObject(relativeTimeFormat) || + (relativeTimeFormat = intl_GuardToRelativeTimeFormat( + relativeTimeFormat + )) === null + ) { + return callFunction( + intl_CallRelativeTimeFormatMethodIfWrapped, + this, + "Intl_RelativeTimeFormat_resolvedOptions" + ); + } + + var internals = getRelativeTimeFormatInternals(relativeTimeFormat); + + // Steps 4-5. + var result = { + locale: internals.locale, + style: internals.style, + numeric: internals.numeric, + numberingSystem: internals.numberingSystem, + }; + + // Step 6. + return result; +} diff --git a/js/src/builtin/intl/SanctionedSimpleUnitIdentifiers.yaml b/js/src/builtin/intl/SanctionedSimpleUnitIdentifiers.yaml new file mode 100644 index 0000000000..97cb44c12c --- /dev/null +++ b/js/src/builtin/intl/SanctionedSimpleUnitIdentifiers.yaml @@ -0,0 +1,58 @@ +# 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/. + +# 6.5.2 IsSanctionedSimpleUnitIdentifier ( unitIdentifier ) +# +# Simple units sanctioned for use in ECMAScript +# +# https://tc39.es/ecma402/#table-sanctioned-simple-unit-identifiers + +# Run |make_intl_data units| to regenerate all files which reference this list +# of sanctioned unit identifiers. + +- acre +- bit +- byte +- celsius +- centimeter +- day +- degree +- fahrenheit +- fluid-ounce +- foot +- gallon +- gigabit +- gigabyte +- gram +- hectare +- hour +- inch +- kilobit +- kilobyte +- kilogram +- kilometer +- liter +- megabit +- megabyte +- meter +- microsecond +- mile +- mile-scandinavian +- milliliter +- millimeter +- millisecond +- minute +- month +- nanosecond +- ounce +- percent +- petabyte +- pound +- second +- stone +- terabit +- terabyte +- week +- yard +- year diff --git a/js/src/builtin/intl/SanctionedSimpleUnitIdentifiersGenerated.js b/js/src/builtin/intl/SanctionedSimpleUnitIdentifiersGenerated.js new file mode 100644 index 0000000000..bc7b460f8e --- /dev/null +++ b/js/src/builtin/intl/SanctionedSimpleUnitIdentifiersGenerated.js @@ -0,0 +1,55 @@ +// Generated by make_intl_data.py. DO NOT EDIT. + +/** + * The list of currently supported simple unit identifiers. + * + * Intl.NumberFormat Unified API Proposal + */ +// prettier-ignore +var sanctionedSimpleUnitIdentifiers = { + "acre": true, + "bit": true, + "byte": true, + "celsius": true, + "centimeter": true, + "day": true, + "degree": true, + "fahrenheit": true, + "fluid-ounce": true, + "foot": true, + "gallon": true, + "gigabit": true, + "gigabyte": true, + "gram": true, + "hectare": true, + "hour": true, + "inch": true, + "kilobit": true, + "kilobyte": true, + "kilogram": true, + "kilometer": true, + "liter": true, + "megabit": true, + "megabyte": true, + "meter": true, + "microsecond": true, + "mile": true, + "mile-scandinavian": true, + "milliliter": true, + "millimeter": true, + "millisecond": true, + "minute": true, + "month": true, + "nanosecond": true, + "ounce": true, + "percent": true, + "petabyte": true, + "pound": true, + "second": true, + "stone": true, + "terabit": true, + "terabyte": true, + "week": true, + "yard": true, + "year": true +}; diff --git a/js/src/builtin/intl/SharedIntlData.cpp b/js/src/builtin/intl/SharedIntlData.cpp new file mode 100644 index 0000000000..18e87b6399 --- /dev/null +++ b/js/src/builtin/intl/SharedIntlData.cpp @@ -0,0 +1,754 @@ +/* -*- 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/. */ + +/* Runtime-wide Intl data shared across compartments. */ + +#include "builtin/intl/SharedIntlData.h" + +#include "mozilla/Assertions.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/intl/Collator.h" +#include "mozilla/intl/DateTimeFormat.h" +#include "mozilla/intl/DateTimePatternGenerator.h" +#include "mozilla/intl/Locale.h" +#include "mozilla/intl/NumberFormat.h" +#include "mozilla/intl/TimeZone.h" +#include "mozilla/Span.h" +#include "mozilla/TextUtils.h" + +#include <algorithm> +#include <stdint.h> +#include <string> +#include <string.h> +#include <string_view> +#include <utility> + +#include "builtin/Array.h" +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/TimeZoneDataGenerated.h" +#include "js/Utility.h" +#include "js/Vector.h" +#include "vm/ArrayObject.h" +#include "vm/JSAtom.h" +#include "vm/JSContext.h" +#include "vm/StringType.h" + +using js::HashNumber; + +template <typename Char> +static constexpr Char ToUpperASCII(Char c) { + return mozilla::IsAsciiLowercaseAlpha(c) ? (c - 0x20) : c; +} + +static_assert(ToUpperASCII('a') == 'A', "verifying 'a' uppercases correctly"); +static_assert(ToUpperASCII('m') == 'M', "verifying 'm' uppercases correctly"); +static_assert(ToUpperASCII('z') == 'Z', "verifying 'z' uppercases correctly"); +static_assert(ToUpperASCII(u'a') == u'A', + "verifying u'a' uppercases correctly"); +static_assert(ToUpperASCII(u'k') == u'K', + "verifying u'k' uppercases correctly"); +static_assert(ToUpperASCII(u'z') == u'Z', + "verifying u'z' uppercases correctly"); + +template <typename Char> +static HashNumber HashStringIgnoreCaseASCII(const Char* s, size_t length) { + uint32_t hash = 0; + for (size_t i = 0; i < length; i++) { + hash = mozilla::AddToHash(hash, ToUpperASCII(s[i])); + } + return hash; +} + +js::intl::SharedIntlData::TimeZoneHasher::Lookup::Lookup( + JSLinearString* timeZone) + : js::intl::SharedIntlData::LinearStringLookup(timeZone) { + if (isLatin1) { + hash = HashStringIgnoreCaseASCII(latin1Chars, length); + } else { + hash = HashStringIgnoreCaseASCII(twoByteChars, length); + } +} + +template <typename Char1, typename Char2> +static bool EqualCharsIgnoreCaseASCII(const Char1* s1, const Char2* s2, + size_t len) { + for (const Char1* s1end = s1 + len; s1 < s1end; s1++, s2++) { + if (ToUpperASCII(*s1) != ToUpperASCII(*s2)) { + return false; + } + } + return true; +} + +bool js::intl::SharedIntlData::TimeZoneHasher::match(TimeZoneName key, + const Lookup& lookup) { + if (key->length() != lookup.length) { + return false; + } + + // Compare time zone names ignoring ASCII case differences. + if (key->hasLatin1Chars()) { + const Latin1Char* keyChars = key->latin1Chars(lookup.nogc); + if (lookup.isLatin1) { + return EqualCharsIgnoreCaseASCII(keyChars, lookup.latin1Chars, + lookup.length); + } + return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, + lookup.length); + } + + const char16_t* keyChars = key->twoByteChars(lookup.nogc); + if (lookup.isLatin1) { + return EqualCharsIgnoreCaseASCII(lookup.latin1Chars, keyChars, + lookup.length); + } + return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, + lookup.length); +} + +static bool IsLegacyICUTimeZone(mozilla::Span<const char> timeZone) { + std::string_view timeZoneView(timeZone.data(), timeZone.size()); + for (const auto& legacyTimeZone : js::timezone::legacyICUTimeZones) { + if (timeZoneView == legacyTimeZone) { + return true; + } + } + return false; +} + +bool js::intl::SharedIntlData::ensureTimeZones(JSContext* cx) { + if (timeZoneDataInitialized) { + return true; + } + + // If ensureTimeZones() was called previously, but didn't complete due to + // OOM, clear all sets/maps and start from scratch. + availableTimeZones.clearAndCompact(); + + auto timeZones = mozilla::intl::TimeZone::GetAvailableTimeZones(); + if (timeZones.isErr()) { + ReportInternalError(cx, timeZones.unwrapErr()); + return false; + } + + Rooted<JSAtom*> timeZone(cx); + for (auto timeZoneName : timeZones.unwrap()) { + if (timeZoneName.isErr()) { + ReportInternalError(cx); + return false; + } + auto timeZoneSpan = timeZoneName.unwrap(); + + // Skip legacy ICU time zone names. + if (IsLegacyICUTimeZone(timeZoneSpan)) { + continue; + } + + timeZone = Atomize(cx, timeZoneSpan.data(), timeZoneSpan.size()); + if (!timeZone) { + return false; + } + + TimeZoneHasher::Lookup lookup(timeZone); + TimeZoneSet::AddPtr p = availableTimeZones.lookupForAdd(lookup); + + // ICU shouldn't report any duplicate time zone names, but if it does, + // just ignore the duplicate name. + if (!p && !availableTimeZones.add(p, timeZone)) { + ReportOutOfMemory(cx); + return false; + } + } + + ianaZonesTreatedAsLinksByICU.clearAndCompact(); + + for (const char* rawTimeZone : timezone::ianaZonesTreatedAsLinksByICU) { + MOZ_ASSERT(rawTimeZone != nullptr); + timeZone = Atomize(cx, rawTimeZone, strlen(rawTimeZone)); + if (!timeZone) { + return false; + } + + TimeZoneHasher::Lookup lookup(timeZone); + TimeZoneSet::AddPtr p = ianaZonesTreatedAsLinksByICU.lookupForAdd(lookup); + MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaZonesTreatedAsLinksByICU"); + + if (!ianaZonesTreatedAsLinksByICU.add(p, timeZone)) { + ReportOutOfMemory(cx); + return false; + } + } + + ianaLinksCanonicalizedDifferentlyByICU.clearAndCompact(); + + Rooted<JSAtom*> linkName(cx); + Rooted<JSAtom*>& target = timeZone; + for (const auto& linkAndTarget : + timezone::ianaLinksCanonicalizedDifferentlyByICU) { + const char* rawLinkName = linkAndTarget.link; + const char* rawTarget = linkAndTarget.target; + + MOZ_ASSERT(rawLinkName != nullptr); + linkName = Atomize(cx, rawLinkName, strlen(rawLinkName)); + if (!linkName) { + return false; + } + + MOZ_ASSERT(rawTarget != nullptr); + target = Atomize(cx, rawTarget, strlen(rawTarget)); + if (!target) { + return false; + } + + TimeZoneHasher::Lookup lookup(linkName); + TimeZoneMap::AddPtr p = + ianaLinksCanonicalizedDifferentlyByICU.lookupForAdd(lookup); + MOZ_ASSERT( + !p, + "Duplicate entry in timezone::ianaLinksCanonicalizedDifferentlyByICU"); + + if (!ianaLinksCanonicalizedDifferentlyByICU.add(p, linkName, target)) { + ReportOutOfMemory(cx); + return false; + } + } + + MOZ_ASSERT(!timeZoneDataInitialized, + "ensureTimeZones is neither reentrant nor thread-safe"); + timeZoneDataInitialized = true; + + return true; +} + +bool js::intl::SharedIntlData::validateTimeZoneName( + JSContext* cx, HandleString timeZone, MutableHandle<JSAtom*> result) { + if (!ensureTimeZones(cx)) { + return false; + } + + Rooted<JSLinearString*> timeZoneLinear(cx, timeZone->ensureLinear(cx)); + if (!timeZoneLinear) { + return false; + } + + TimeZoneHasher::Lookup lookup(timeZoneLinear); + if (TimeZoneSet::Ptr p = availableTimeZones.lookup(lookup)) { + result.set(*p); + } + + return true; +} + +bool js::intl::SharedIntlData::tryCanonicalizeTimeZoneConsistentWithIANA( + JSContext* cx, HandleString timeZone, MutableHandle<JSAtom*> result) { + if (!ensureTimeZones(cx)) { + return false; + } + + Rooted<JSLinearString*> timeZoneLinear(cx, timeZone->ensureLinear(cx)); + if (!timeZoneLinear) { + return false; + } + + TimeZoneHasher::Lookup lookup(timeZoneLinear); + MOZ_ASSERT(availableTimeZones.has(lookup), "Invalid time zone name"); + + if (TimeZoneMap::Ptr p = + ianaLinksCanonicalizedDifferentlyByICU.lookup(lookup)) { + // The effectively supported time zones aren't known at compile time, + // when + // 1. SpiderMonkey was compiled with "--with-system-icu". + // 2. ICU's dynamic time zone data loading feature was used. + // (ICU supports loading time zone files at runtime through the + // ICU_TIMEZONE_FILES_DIR environment variable.) + // Ensure ICU supports the new target zone before applying the update. + TimeZoneName targetTimeZone = p->value(); + TimeZoneHasher::Lookup targetLookup(targetTimeZone); + if (availableTimeZones.has(targetLookup)) { + result.set(targetTimeZone); + } + } else if (TimeZoneSet::Ptr p = ianaZonesTreatedAsLinksByICU.lookup(lookup)) { + result.set(*p); + } + + return true; +} + +JS::Result<js::intl::SharedIntlData::TimeZoneSet::Iterator> +js::intl::SharedIntlData::availableTimeZonesIteration(JSContext* cx) { + if (!ensureTimeZones(cx)) { + return cx->alreadyReportedError(); + } + return availableTimeZones.iter(); +} + +js::intl::SharedIntlData::LocaleHasher::Lookup::Lookup(JSLinearString* locale) + : js::intl::SharedIntlData::LinearStringLookup(locale) { + if (isLatin1) { + hash = mozilla::HashString(latin1Chars, length); + } else { + hash = mozilla::HashString(twoByteChars, length); + } +} + +js::intl::SharedIntlData::LocaleHasher::Lookup::Lookup(const char* chars, + size_t length) + : js::intl::SharedIntlData::LinearStringLookup(chars, length) { + hash = mozilla::HashString(latin1Chars, length); +} + +bool js::intl::SharedIntlData::LocaleHasher::match(Locale key, + const Lookup& lookup) { + if (key->length() != lookup.length) { + return false; + } + + if (key->hasLatin1Chars()) { + const Latin1Char* keyChars = key->latin1Chars(lookup.nogc); + if (lookup.isLatin1) { + return EqualChars(keyChars, lookup.latin1Chars, lookup.length); + } + return EqualChars(keyChars, lookup.twoByteChars, lookup.length); + } + + const char16_t* keyChars = key->twoByteChars(lookup.nogc); + if (lookup.isLatin1) { + return EqualChars(lookup.latin1Chars, keyChars, lookup.length); + } + return EqualChars(keyChars, lookup.twoByteChars, lookup.length); +} + +template <class AvailableLocales> +bool js::intl::SharedIntlData::getAvailableLocales( + JSContext* cx, LocaleSet& locales, + const AvailableLocales& availableLocales) { + auto addLocale = [cx, &locales](const char* locale, size_t length) { + JSAtom* atom = Atomize(cx, locale, length); + if (!atom) { + return false; + } + + LocaleHasher::Lookup lookup(atom); + LocaleSet::AddPtr p = locales.lookupForAdd(lookup); + + // ICU shouldn't report any duplicate locales, but if it does, just + // ignore the duplicated locale. + if (!p && !locales.add(p, atom)) { + ReportOutOfMemory(cx); + return false; + } + + return true; + }; + + js::Vector<char, 16> lang(cx); + + for (const char* locale : availableLocales) { + size_t length = strlen(locale); + + lang.clear(); + if (!lang.append(locale, length)) { + return false; + } + MOZ_ASSERT(lang.length() == length); + + std::replace(lang.begin(), lang.end(), '_', '-'); + + if (!addLocale(lang.begin(), length)) { + return false; + } + + // From <https://tc39.es/ecma402/#sec-internal-slots>: + // + // For locales that include a script subtag in addition to language and + // region, the corresponding locale without a script subtag must also be + // supported; that is, if an implementation recognizes "zh-Hant-TW", it is + // also expected to recognize "zh-TW". + + // 2 * Alpha language subtag + // + 1 separator + // + 4 * Alphanum script subtag + // + 1 separator + // + 2 * Alpha region subtag + using namespace mozilla::intl::LanguageTagLimits; + static constexpr size_t MinLanguageLength = 2; + static constexpr size_t MinLengthForScriptAndRegion = + MinLanguageLength + 1 + ScriptLength + 1 + AlphaRegionLength; + + // Fast case: Skip locales without script subtags. + if (length < MinLengthForScriptAndRegion) { + continue; + } + + // We don't need the full-fledged language tag parser when we just want to + // remove the script subtag. + + // Find the separator between the language and script subtags. + const char* sep = std::char_traits<char>::find(lang.begin(), length, '-'); + if (!sep) { + continue; + } + + // Possible |script| subtag start position. + const char* script = sep + 1; + + // Find the separator between the script and region subtags. + sep = std::char_traits<char>::find(script, lang.end() - script, '-'); + if (!sep) { + continue; + } + + // Continue with the next locale if we didn't find a script subtag. + size_t scriptLength = sep - script; + if (!mozilla::intl::IsStructurallyValidScriptTag<char>( + {script, scriptLength})) { + continue; + } + + // Possible |region| subtag start position. + const char* region = sep + 1; + + // Search if there's yet another subtag after the region subtag. + sep = std::char_traits<char>::find(region, lang.end() - region, '-'); + + // Continue with the next locale if we didn't find a region subtag. + size_t regionLength = (sep ? sep : lang.end()) - region; + if (!mozilla::intl::IsStructurallyValidRegionTag<char>( + {region, regionLength})) { + continue; + } + + // We've found a script and a region subtag. + + static constexpr size_t ScriptWithSeparatorLength = ScriptLength + 1; + + // Remove the script subtag. Note: erase() needs non-const pointers, which + // means we can't directly pass |script|. + char* p = const_cast<char*>(script); + lang.erase(p, p + ScriptWithSeparatorLength); + + MOZ_ASSERT(lang.length() == length - ScriptWithSeparatorLength); + + // Add the locale with the script subtag removed. + if (!addLocale(lang.begin(), lang.length())) { + return false; + } + } + + // Forcibly add an entry for the last-ditch locale, in case ICU doesn't + // directly support it (but does support it through fallback, e.g. supporting + // "en-GB" indirectly using "en" support). + { + const char* lastDitch = intl::LastDitchLocale(); + MOZ_ASSERT(strcmp(lastDitch, "en-GB") == 0); + +#ifdef DEBUG + static constexpr char lastDitchParent[] = "en"; + + LocaleHasher::Lookup lookup(lastDitchParent, strlen(lastDitchParent)); + MOZ_ASSERT(locales.has(lookup), + "shouldn't be a need to add every locale implied by the " + "last-ditch locale, merely just the last-ditch locale"); +#endif + + if (!addLocale(lastDitch, strlen(lastDitch))) { + return false; + } + } + + return true; +} + +#ifdef DEBUG +template <class AvailableLocales1, class AvailableLocales2> +static bool IsSameAvailableLocales(const AvailableLocales1& availableLocales1, + const AvailableLocales2& availableLocales2) { + return std::equal(std::begin(availableLocales1), std::end(availableLocales1), + std::begin(availableLocales2), std::end(availableLocales2), + [](const char* a, const char* b) { + // Intentionally comparing pointer equivalence. + return a == b; + }); +} +#endif + +bool js::intl::SharedIntlData::ensureSupportedLocales(JSContext* cx) { + if (supportedLocalesInitialized) { + return true; + } + + // If ensureSupportedLocales() was called previously, but didn't complete due + // to OOM, clear all data and start from scratch. + supportedLocales.clearAndCompact(); + collatorSupportedLocales.clearAndCompact(); + + if (!getAvailableLocales(cx, supportedLocales, + mozilla::intl::Locale::GetAvailableLocales())) { + return false; + } + if (!getAvailableLocales(cx, collatorSupportedLocales, + mozilla::intl::Collator::GetAvailableLocales())) { + return false; + } + + MOZ_ASSERT(IsSameAvailableLocales( + mozilla::intl::Locale::GetAvailableLocales(), + mozilla::intl::DateTimeFormat::GetAvailableLocales())); + + MOZ_ASSERT(IsSameAvailableLocales( + mozilla::intl::Locale::GetAvailableLocales(), + mozilla::intl::NumberFormat::GetAvailableLocales())); + + MOZ_ASSERT(!supportedLocalesInitialized, + "ensureSupportedLocales is neither reentrant nor thread-safe"); + supportedLocalesInitialized = true; + + return true; +} + +bool js::intl::SharedIntlData::isSupportedLocale(JSContext* cx, + SupportedLocaleKind kind, + HandleString locale, + bool* supported) { + if (!ensureSupportedLocales(cx)) { + return false; + } + + Rooted<JSLinearString*> localeLinear(cx, locale->ensureLinear(cx)); + if (!localeLinear) { + return false; + } + + LocaleHasher::Lookup lookup(localeLinear); + + switch (kind) { + case SupportedLocaleKind::Collator: + *supported = collatorSupportedLocales.has(lookup); + return true; + case SupportedLocaleKind::DateTimeFormat: + case SupportedLocaleKind::DisplayNames: + case SupportedLocaleKind::ListFormat: + case SupportedLocaleKind::NumberFormat: + case SupportedLocaleKind::PluralRules: + case SupportedLocaleKind::RelativeTimeFormat: + *supported = supportedLocales.has(lookup); + return true; + } + MOZ_CRASH("Invalid Intl constructor"); +} + +js::ArrayObject* js::intl::SharedIntlData::availableLocalesOf( + JSContext* cx, SupportedLocaleKind kind) { + if (!ensureSupportedLocales(cx)) { + return nullptr; + } + + LocaleSet* localeSet = nullptr; + switch (kind) { + case SupportedLocaleKind::Collator: + localeSet = &collatorSupportedLocales; + break; + case SupportedLocaleKind::DateTimeFormat: + case SupportedLocaleKind::DisplayNames: + case SupportedLocaleKind::ListFormat: + case SupportedLocaleKind::NumberFormat: + case SupportedLocaleKind::PluralRules: + case SupportedLocaleKind::RelativeTimeFormat: + localeSet = &supportedLocales; + break; + default: + MOZ_CRASH("Invalid Intl constructor"); + } + + const uint32_t count = localeSet->count(); + ArrayObject* result = NewDenseFullyAllocatedArray(cx, count); + if (!result) { + return nullptr; + } + result->setDenseInitializedLength(count); + + uint32_t index = 0; + for (auto range = localeSet->iter(); !range.done(); range.next()) { + JSAtom* locale = range.get(); + cx->markAtom(locale); + + result->initDenseElement(index++, StringValue(locale)); + } + MOZ_ASSERT(index == count); + + return result; +} + +#if DEBUG || MOZ_SYSTEM_ICU +bool js::intl::SharedIntlData::ensureUpperCaseFirstLocales(JSContext* cx) { + if (upperCaseFirstInitialized) { + return true; + } + + // If ensureUpperCaseFirstLocales() was called previously, but didn't + // complete due to OOM, clear all data and start from scratch. + upperCaseFirstLocales.clearAndCompact(); + + Rooted<JSAtom*> locale(cx); + for (const char* rawLocale : mozilla::intl::Collator::GetAvailableLocales()) { + auto collator = mozilla::intl::Collator::TryCreate(rawLocale); + if (collator.isErr()) { + ReportInternalError(cx, collator.unwrapErr()); + return false; + } + + auto caseFirst = collator.unwrap()->GetCaseFirst(); + if (caseFirst.isErr()) { + ReportInternalError(cx, caseFirst.unwrapErr()); + return false; + } + + if (caseFirst.unwrap() != mozilla::intl::Collator::CaseFirst::Upper) { + continue; + } + + locale = Atomize(cx, rawLocale, strlen(rawLocale)); + if (!locale) { + return false; + } + + LocaleHasher::Lookup lookup(locale); + LocaleSet::AddPtr p = upperCaseFirstLocales.lookupForAdd(lookup); + + // ICU shouldn't report any duplicate locales, but if it does, just + // ignore the duplicated locale. + if (!p && !upperCaseFirstLocales.add(p, locale)) { + ReportOutOfMemory(cx); + return false; + } + } + + MOZ_ASSERT( + !upperCaseFirstInitialized, + "ensureUpperCaseFirstLocales is neither reentrant nor thread-safe"); + upperCaseFirstInitialized = true; + + return true; +} +#endif // DEBUG || MOZ_SYSTEM_ICU + +bool js::intl::SharedIntlData::isUpperCaseFirst(JSContext* cx, + HandleString locale, + bool* isUpperFirst) { +#if DEBUG || MOZ_SYSTEM_ICU + if (!ensureUpperCaseFirstLocales(cx)) { + return false; + } +#endif + + Rooted<JSLinearString*> localeLinear(cx, locale->ensureLinear(cx)); + if (!localeLinear) { + return false; + } + +#if !MOZ_SYSTEM_ICU + // "da" (Danish) and "mt" (Maltese) are the only two supported locales using + // upper-case first. CLDR also lists "cu" (Church Slavic) as an upper-case + // first locale, but since it's not supported in ICU, we don't care about it + // here. + bool isDefaultUpperCaseFirstLocale = + js::StringEqualsLiteral(localeLinear, "da") || + js::StringEqualsLiteral(localeLinear, "mt"); +#endif + +#if DEBUG || MOZ_SYSTEM_ICU + LocaleHasher::Lookup lookup(localeLinear); + *isUpperFirst = upperCaseFirstLocales.has(lookup); +#else + *isUpperFirst = isDefaultUpperCaseFirstLocale; +#endif + +#if !MOZ_SYSTEM_ICU + MOZ_ASSERT(*isUpperFirst == isDefaultUpperCaseFirstLocale, + "upper-case first locales don't match hard-coded list"); +#endif + + return true; +} + +void js::intl::DateTimePatternGeneratorDeleter::operator()( + mozilla::intl::DateTimePatternGenerator* ptr) { + delete ptr; +} + +static bool StringsAreEqual(const char* s1, const char* s2) { + return !strcmp(s1, s2); +} + +mozilla::intl::DateTimePatternGenerator* +js::intl::SharedIntlData::getDateTimePatternGenerator(JSContext* cx, + const char* locale) { + // Return the cached instance if the requested locale matches the locale + // of the cached generator. + if (dateTimePatternGeneratorLocale && + StringsAreEqual(dateTimePatternGeneratorLocale.get(), locale)) { + return dateTimePatternGenerator.get(); + } + + auto result = mozilla::intl::DateTimePatternGenerator::TryCreate(locale); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + // The UniquePtr needs to be recreated as it's using a different Deleter in + // order to be able to forward declare DateTimePatternGenerator in + // SharedIntlData.h. + UniqueDateTimePatternGenerator gen(result.unwrap().release()); + + JS::UniqueChars localeCopy = js::DuplicateString(cx, locale); + if (!localeCopy) { + return nullptr; + } + + dateTimePatternGenerator = std::move(gen); + dateTimePatternGeneratorLocale = std::move(localeCopy); + + return dateTimePatternGenerator.get(); +} + +void js::intl::SharedIntlData::destroyInstance() { + availableTimeZones.clearAndCompact(); + ianaZonesTreatedAsLinksByICU.clearAndCompact(); + ianaLinksCanonicalizedDifferentlyByICU.clearAndCompact(); + supportedLocales.clearAndCompact(); + collatorSupportedLocales.clearAndCompact(); +#if DEBUG || MOZ_SYSTEM_ICU + upperCaseFirstLocales.clearAndCompact(); +#endif +} + +void js::intl::SharedIntlData::trace(JSTracer* trc) { + // Atoms are always tenured. + if (!JS::RuntimeHeapIsMinorCollecting()) { + availableTimeZones.trace(trc); + ianaZonesTreatedAsLinksByICU.trace(trc); + ianaLinksCanonicalizedDifferentlyByICU.trace(trc); + supportedLocales.trace(trc); + collatorSupportedLocales.trace(trc); +#if DEBUG || MOZ_SYSTEM_ICU + upperCaseFirstLocales.trace(trc); +#endif + } +} + +size_t js::intl::SharedIntlData::sizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + return availableTimeZones.shallowSizeOfExcludingThis(mallocSizeOf) + + ianaZonesTreatedAsLinksByICU.shallowSizeOfExcludingThis(mallocSizeOf) + + ianaLinksCanonicalizedDifferentlyByICU.shallowSizeOfExcludingThis( + mallocSizeOf) + + supportedLocales.shallowSizeOfExcludingThis(mallocSizeOf) + + collatorSupportedLocales.shallowSizeOfExcludingThis(mallocSizeOf) + +#if DEBUG || MOZ_SYSTEM_ICU + upperCaseFirstLocales.shallowSizeOfExcludingThis(mallocSizeOf) + +#endif + mallocSizeOf(dateTimePatternGeneratorLocale.get()); +} diff --git a/js/src/builtin/intl/SharedIntlData.h b/js/src/builtin/intl/SharedIntlData.h new file mode 100644 index 0000000000..8cada7c61b --- /dev/null +++ b/js/src/builtin/intl/SharedIntlData.h @@ -0,0 +1,335 @@ +/* -*- 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 builtin_intl_SharedIntlData_h +#define builtin_intl_SharedIntlData_h + +#include "mozilla/MemoryReporting.h" +#include "mozilla/UniquePtr.h" + +#include <stddef.h> + +#include "js/AllocPolicy.h" +#include "js/GCAPI.h" +#include "js/GCHashTable.h" +#include "js/Result.h" +#include "js/RootingAPI.h" +#include "js/Utility.h" +#include "vm/StringType.h" + +namespace mozilla::intl { +class DateTimePatternGenerator; +} // namespace mozilla::intl + +namespace js { + +class ArrayObject; + +namespace intl { + +/** + * This deleter class exists so that mozilla::intl::DateTimePatternGenerator + * can be a forward declaration, but still be used inside of a UniquePtr. + */ +class DateTimePatternGeneratorDeleter { + public: + void operator()(mozilla::intl::DateTimePatternGenerator* ptr); +}; + +/** + * Stores Intl data which can be shared across compartments (but not contexts). + * + * Used for data which is expensive when computed repeatedly or is not + * available through ICU. + */ +class SharedIntlData { + struct LinearStringLookup { + union { + const JS::Latin1Char* latin1Chars; + const char16_t* twoByteChars; + }; + bool isLatin1; + size_t length; + JS::AutoCheckCannotGC nogc; + HashNumber hash = 0; + + explicit LinearStringLookup(JSLinearString* string) + : isLatin1(string->hasLatin1Chars()), length(string->length()) { + if (isLatin1) { + latin1Chars = string->latin1Chars(nogc); + } else { + twoByteChars = string->twoByteChars(nogc); + } + } + + LinearStringLookup(const char* chars, size_t length) + : isLatin1(true), length(length) { + latin1Chars = reinterpret_cast<const JS::Latin1Char*>(chars); + } + }; + + public: + /** + * Information tracking the set of the supported time zone names, derived + * from the IANA time zone database <https://www.iana.org/time-zones>. + * + * There are two kinds of IANA time zone names: Zone and Link (denoted as + * such in database source files). Zone names are the canonical, preferred + * name for a time zone, e.g. Asia/Kolkata. Link names simply refer to + * target Zone names for their meaning, e.g. Asia/Calcutta targets + * Asia/Kolkata. That a name is a Link doesn't *necessarily* reflect a + * sense of deprecation: some Link names also exist partly for convenience, + * e.g. UTC and GMT as Link names targeting the Zone name Etc/UTC. + * + * Two data sources determine the time zone names we support: those ICU + * supports and IANA's zone information. + * + * Unfortunately the names ICU and IANA support, and their Link + * relationships from name to target, aren't identical, so we can't simply + * implicitly trust ICU's name handling. We must perform various + * preprocessing of user-provided zone names and post-processing of + * ICU-provided zone names to implement ECMA-402's IANA-consistent behavior. + * + * Also see <https://ssl.icu-project.org/trac/ticket/12044> and + * <http://unicode.org/cldr/trac/ticket/9892>. + */ + + using TimeZoneName = JSAtom*; + + struct TimeZoneHasher { + struct Lookup : LinearStringLookup { + explicit Lookup(JSLinearString* timeZone); + }; + + static js::HashNumber hash(const Lookup& lookup) { return lookup.hash; } + static bool match(TimeZoneName key, const Lookup& lookup); + }; + + using TimeZoneSet = + GCHashSet<TimeZoneName, TimeZoneHasher, SystemAllocPolicy>; + using TimeZoneMap = + GCHashMap<TimeZoneName, TimeZoneName, TimeZoneHasher, SystemAllocPolicy>; + + private: + /** + * As a threshold matter, available time zones are those time zones ICU + * supports, via ucal_openTimeZones. But ICU supports additional non-IANA + * time zones described in intl/icu/source/tools/tzcode/icuzones (listed in + * IntlTimeZoneData.cpp's |legacyICUTimeZones|) for its own backwards + * compatibility purposes. This set consists of ICU's supported time zones, + * minus all backwards-compatibility time zones. + */ + TimeZoneSet availableTimeZones; + + /** + * IANA treats some time zone names as Zones, that ICU instead treats as + * Links. For example, IANA considers "America/Indiana/Indianapolis" to be + * a Zone and "America/Fort_Wayne" a Link that targets it, but ICU + * considers the former a Link that targets "America/Indianapolis" (which + * IANA treats as a Link). + * + * ECMA-402 requires that we respect IANA data, so if we're asked to + * canonicalize a time zone name in this set, we must *not* return ICU's + * canonicalization. + */ + TimeZoneSet ianaZonesTreatedAsLinksByICU; + + /** + * IANA treats some time zone names as Links to one target, that ICU + * instead treats as either Zones, or Links to different targets. An + * example of the former is "Asia/Calcutta, which IANA assigns the target + * "Asia/Kolkata" but ICU considers its own Zone. An example of the latter + * is "America/Virgin", which IANA assigns the target + * "America/Port_of_Spain" but ICU assigns the target "America/St_Thomas". + * + * ECMA-402 requires that we respect IANA data, so if we're asked to + * canonicalize a time zone name that's a key in this map, we *must* return + * the corresponding value and *must not* return ICU's canonicalization. + */ + TimeZoneMap ianaLinksCanonicalizedDifferentlyByICU; + + bool timeZoneDataInitialized = false; + + /** + * Precomputes the available time zone names, because it's too expensive to + * call ucal_openTimeZones() repeatedly. + */ + bool ensureTimeZones(JSContext* cx); + + public: + /** + * Returns the validated time zone name in |result|. If the input time zone + * isn't a valid IANA time zone name, |result| remains unchanged. + */ + bool validateTimeZoneName(JSContext* cx, JS::Handle<JSString*> timeZone, + JS::MutableHandle<JSAtom*> result); + + /** + * Returns the canonical time zone name in |result|. If no canonical name + * was found, |result| remains unchanged. + * + * This method only handles time zones which are canonicalized differently + * by ICU when compared to IANA. + */ + bool tryCanonicalizeTimeZoneConsistentWithIANA( + JSContext* cx, JS::Handle<JSString*> timeZone, + JS::MutableHandle<JSAtom*> result); + + /** + * Returns an iterator over all available time zones supported by ICU. The + * returned time zone names aren't canonicalized. + */ + JS::Result<TimeZoneSet::Iterator> availableTimeZonesIteration(JSContext* cx); + + private: + using Locale = JSAtom*; + + struct LocaleHasher { + struct Lookup : LinearStringLookup { + explicit Lookup(JSLinearString* locale); + Lookup(const char* chars, size_t length); + }; + + static js::HashNumber hash(const Lookup& lookup) { return lookup.hash; } + static bool match(Locale key, const Lookup& lookup); + }; + + using LocaleSet = GCHashSet<Locale, LocaleHasher, SystemAllocPolicy>; + + // Set of supported locales for all Intl service constructors except Collator, + // which uses its own set. + // + // UDateFormat: + // udat_[count,get]Available() return the same results as their + // uloc_[count,get]Available() counterparts. + // + // UNumberFormatter: + // unum_[count,get]Available() return the same results as their + // uloc_[count,get]Available() counterparts. + // + // UListFormatter, UPluralRules, and URelativeDateTimeFormatter: + // We're going to use ULocale availableLocales as per ICU recommendation: + // https://unicode-org.atlassian.net/browse/ICU-12756 + LocaleSet supportedLocales; + + // ucol_[count,get]Available() return different results compared to + // uloc_[count,get]Available(), we can't use |supportedLocales| here. + LocaleSet collatorSupportedLocales; + + bool supportedLocalesInitialized = false; + + // CountAvailable and GetAvailable describe the signatures used for ICU API + // to determine available locales for various functionality. + using CountAvailable = int32_t (*)(); + using GetAvailable = const char* (*)(int32_t localeIndex); + + template <class AvailableLocales> + static bool getAvailableLocales(JSContext* cx, LocaleSet& locales, + const AvailableLocales& availableLocales); + + /** + * Precomputes the available locales sets. + */ + bool ensureSupportedLocales(JSContext* cx); + + public: + enum class SupportedLocaleKind { + Collator, + DateTimeFormat, + DisplayNames, + ListFormat, + NumberFormat, + PluralRules, + RelativeTimeFormat + }; + + /** + * Sets |supported| to true if |locale| is supported by the requested Intl + * service constructor. Otherwise sets |supported| to false. + */ + [[nodiscard]] bool isSupportedLocale(JSContext* cx, SupportedLocaleKind kind, + JS::Handle<JSString*> locale, + bool* supported); + + /** + * Returns all available locales for |kind|. + */ + ArrayObject* availableLocalesOf(JSContext* cx, SupportedLocaleKind kind); + + private: + /** + * The case first parameter (BCP47 key "kf") allows to switch the order of + * upper- and lower-case characters. ICU doesn't directly provide an API + * to query the default case first value of a given locale, but instead + * requires to instantiate a collator object and then query the case first + * attribute (UCOL_CASE_FIRST). + * To avoid instantiating an additional collator object whenever we need + * to retrieve the default case first value of a specific locale, we + * compute the default case first value for every supported locale only + * once and then keep a list of all locales which don't use the default + * case first setting. + * There is almost no difference between lower-case first and when case + * first is disabled (UCOL_LOWER_FIRST resp. UCOL_OFF), so we only need to + * track locales which use upper-case first as their default setting. + * + * Instantiating collator objects for each available locale is slow + * (bug 1527879), therefore we're hardcoding the two locales using upper-case + * first ("da" (Danish) and "mt" (Maltese)) and only assert in debug-mode + * these two locales match the upper-case first locales returned by ICU. A + * system-ICU may support a different set of locales, therefore we're always + * calling into ICU to find the upper-case first locales in that case. + */ + +#if DEBUG || MOZ_SYSTEM_ICU + LocaleSet upperCaseFirstLocales; + + bool upperCaseFirstInitialized = false; + + /** + * Precomputes the available locales which use upper-case first sorting. + */ + bool ensureUpperCaseFirstLocales(JSContext* cx); +#endif + + public: + /** + * Sets |isUpperFirst| to true if |locale| sorts upper-case characters + * before lower-case characters. + */ + bool isUpperCaseFirst(JSContext* cx, JS::Handle<JSString*> locale, + bool* isUpperFirst); + + private: + using UniqueDateTimePatternGenerator = + mozilla::UniquePtr<mozilla::intl::DateTimePatternGenerator, + DateTimePatternGeneratorDeleter>; + + UniqueDateTimePatternGenerator dateTimePatternGenerator; + JS::UniqueChars dateTimePatternGeneratorLocale; + + public: + /** + * Get a non-owned cached instance of the DateTimePatternGenerator, which is + * expensive to instantiate. + * + * See: https://bugzilla.mozilla.org/show_bug.cgi?id=1549578 + */ + mozilla::intl::DateTimePatternGenerator* getDateTimePatternGenerator( + JSContext* cx, const char* locale); + + public: + void destroyInstance(); + + void trace(JSTracer* trc); + + size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; +}; + +} // namespace intl + +} // namespace js + +#endif /* builtin_intl_SharedIntlData_h */ diff --git a/js/src/builtin/intl/StringAsciiChars.h b/js/src/builtin/intl/StringAsciiChars.h new file mode 100644 index 0000000000..3323544d8c --- /dev/null +++ b/js/src/builtin/intl/StringAsciiChars.h @@ -0,0 +1,77 @@ +/* -*- 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 builtin_intl_StringAsciiChars_h +#define builtin_intl_StringAsciiChars_h + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/Span.h" + +#include <stddef.h> + +#include "js/GCAPI.h" +#include "js/TypeDecls.h" +#include "js/Vector.h" + +#include "vm/StringType.h" + +namespace js::intl { + +/** + * String view of an ASCII-only string. + * + * This holds a reference to a JSLinearString and can produce a string view + * into that string. If the string is represented by Latin1 characters, the + * span is returned directly. If the string is represented by UTF-16 + * characters, it copies the char16_t characters into a char array, and then + * returns a span based on the copy. + * + * This allows us to avoid copying for the common use case that the ASCII + * characters are represented in Latin1. + */ +class MOZ_STACK_CLASS StringAsciiChars final { + // When copying string characters, use this many bytes of inline storage. + static const size_t InlineCapacity = 24; + + JS::AutoCheckCannotGC nogc_; + + JSLinearString* str_; + + mozilla::Maybe<Vector<Latin1Char, InlineCapacity>> ownChars_; + + public: + explicit StringAsciiChars(JSLinearString* str) : str_(str) { + MOZ_ASSERT(StringIsAscii(str)); + } + + operator mozilla::Span<const char>() const { + if (str_->hasLatin1Chars()) { + return mozilla::AsChars(str_->latin1Range(nogc_)); + } + return mozilla::AsChars(mozilla::Span<const Latin1Char>(*ownChars_)); + } + + [[nodiscard]] bool init(JSContext* cx) { + if (str_->hasLatin1Chars()) { + return true; + } + + ownChars_.emplace(cx); + if (!ownChars_->resize(str_->length())) { + return false; + } + + js::CopyChars(ownChars_->begin(), *str_); + + return true; + } +}; + +} // namespace js::intl + +#endif // builtin_intl_StringAsciiChars_h diff --git a/js/src/builtin/intl/TimeZoneDataGenerated.h b/js/src/builtin/intl/TimeZoneDataGenerated.h new file mode 100644 index 0000000000..7757e11402 --- /dev/null +++ b/js/src/builtin/intl/TimeZoneDataGenerated.h @@ -0,0 +1,142 @@ +// Generated by make_intl_data.py. DO NOT EDIT. +// tzdata version = 2023c + +#ifndef builtin_intl_TimeZoneDataGenerated_h +#define builtin_intl_TimeZoneDataGenerated_h + +namespace js { +namespace timezone { + +// Format: +// "ZoneName" // ICU-Name [time zone file] +const char* const ianaZonesTreatedAsLinksByICU[] = { + "Africa/Asmara", // Africa/Asmera [backzone] + "Africa/Timbuktu", // Africa/Bamako [backzone] + "America/Argentina/Buenos_Aires", // America/Buenos_Aires [southamerica] + "America/Argentina/Catamarca", // America/Catamarca [southamerica] + "America/Argentina/ComodRivadavia", // America/Catamarca [backzone] + "America/Argentina/Cordoba", // America/Cordoba [southamerica] + "America/Argentina/Jujuy", // America/Jujuy [southamerica] + "America/Argentina/Mendoza", // America/Mendoza [southamerica] + "America/Atikokan", // America/Coral_Harbour [backzone] + "America/Ensenada", // America/Tijuana [backzone] + "America/Indiana/Indianapolis", // America/Indianapolis [northamerica] + "America/Kentucky/Louisville", // America/Louisville [northamerica] + "America/Nuuk", // America/Godthab [europe] + "America/Rosario", // America/Cordoba [backzone] + "Asia/Chongqing", // Asia/Shanghai [backzone] + "Asia/Harbin", // Asia/Shanghai [backzone] + "Asia/Ho_Chi_Minh", // Asia/Saigon [asia] + "Asia/Kashgar", // Asia/Urumqi [backzone] + "Asia/Kathmandu", // Asia/Katmandu [asia] + "Asia/Kolkata", // Asia/Calcutta [asia] + "Asia/Tel_Aviv", // Asia/Jerusalem [backzone] + "Asia/Yangon", // Asia/Rangoon [asia] + "Atlantic/Faroe", // Atlantic/Faeroe [europe] + "Atlantic/Jan_Mayen", // Arctic/Longyearbyen [backzone] + "EST", // Etc/GMT+5 [northamerica] + "Europe/Belfast", // Europe/London [backzone] + "Europe/Kyiv", // Europe/Kiev [europe] + "Europe/Tiraspol", // Europe/Chisinau [backzone] + "HST", // Etc/GMT+10 [northamerica] + "MST", // Etc/GMT+7 [northamerica] + "Pacific/Chuuk", // Pacific/Truk [backzone] + "Pacific/Kanton", // Pacific/Enderbury [australasia] + "Pacific/Pohnpei", // Pacific/Ponape [backzone] +}; + +// Format: +// "LinkName", "Target" // ICU-Target [time zone file] +struct LinkAndTarget +{ + const char* const link; + const char* const target; +}; + +const LinkAndTarget ianaLinksCanonicalizedDifferentlyByICU[] = { + { "Africa/Asmera", "Africa/Asmara" }, // Africa/Asmera [backward] + { "America/Buenos_Aires", "America/Argentina/Buenos_Aires" }, // America/Buenos_Aires [backward] + { "America/Catamarca", "America/Argentina/Catamarca" }, // America/Catamarca [backward] + { "America/Cordoba", "America/Argentina/Cordoba" }, // America/Cordoba [backward] + { "America/Fort_Wayne", "America/Indiana/Indianapolis" }, // America/Indianapolis [backward] + { "America/Godthab", "America/Nuuk" }, // America/Godthab [backward] + { "America/Indianapolis", "America/Indiana/Indianapolis" }, // America/Indianapolis [backward] + { "America/Jujuy", "America/Argentina/Jujuy" }, // America/Jujuy [backward] + { "America/Kralendijk", "America/Curacao" }, // America/Kralendijk [backward] + { "America/Louisville", "America/Kentucky/Louisville" }, // America/Louisville [backward] + { "America/Lower_Princes", "America/Curacao" }, // America/Lower_Princes [backward] + { "America/Marigot", "America/Port_of_Spain" }, // America/Marigot [backward] + { "America/Mendoza", "America/Argentina/Mendoza" }, // America/Mendoza [backward] + { "America/Santa_Isabel", "America/Tijuana" }, // America/Santa_Isabel [backward] + { "America/St_Barthelemy", "America/Port_of_Spain" }, // America/St_Barthelemy [backward] + { "Antarctica/South_Pole", "Antarctica/McMurdo" }, // Pacific/Auckland [backward] + { "Arctic/Longyearbyen", "Europe/Oslo" }, // Arctic/Longyearbyen [backward] + { "Asia/Calcutta", "Asia/Kolkata" }, // Asia/Calcutta [backward] + { "Asia/Chungking", "Asia/Chongqing" }, // Asia/Shanghai [backward] + { "Asia/Katmandu", "Asia/Kathmandu" }, // Asia/Katmandu [backward] + { "Asia/Rangoon", "Asia/Yangon" }, // Asia/Rangoon [backward] + { "Asia/Saigon", "Asia/Ho_Chi_Minh" }, // Asia/Saigon [backward] + { "Atlantic/Faeroe", "Atlantic/Faroe" }, // Atlantic/Faeroe [backward] + { "Europe/Bratislava", "Europe/Prague" }, // Europe/Bratislava [backward] + { "Europe/Busingen", "Europe/Zurich" }, // Europe/Busingen [backward] + { "Europe/Kiev", "Europe/Kyiv" }, // Europe/Kiev [backward] + { "Europe/Mariehamn", "Europe/Helsinki" }, // Europe/Mariehamn [backward] + { "Europe/Podgorica", "Europe/Belgrade" }, // Europe/Podgorica [backward] + { "Europe/San_Marino", "Europe/Rome" }, // Europe/San_Marino [backward] + { "Europe/Vatican", "Europe/Rome" }, // Europe/Vatican [backward] + { "Pacific/Ponape", "Pacific/Pohnpei" }, // Pacific/Ponape [backward] + { "Pacific/Truk", "Pacific/Chuuk" }, // Pacific/Truk [backward] + { "Pacific/Yap", "Pacific/Chuuk" }, // Pacific/Truk [backward] + { "US/East-Indiana", "America/Indiana/Indianapolis" }, // America/Indianapolis [backward] +}; + +// Legacy ICU time zones, these are not valid IANA time zone names. We also +// disallow the old and deprecated System V time zones. +// https://ssl.icu-project.org/repos/icu/trunk/icu4c/source/tools/tzcode/icuzones +const char* const legacyICUTimeZones[] = { + "ACT", + "AET", + "AGT", + "ART", + "AST", + "BET", + "BST", + "CAT", + "CNT", + "CST", + "CTT", + "Canada/East-Saskatchewan", + "EAT", + "ECT", + "IET", + "IST", + "JST", + "MIT", + "NET", + "NST", + "PLT", + "PNT", + "PRT", + "PST", + "SST", + "US/Pacific-New", + "VST", + "SystemV/AST4", + "SystemV/AST4ADT", + "SystemV/CST6", + "SystemV/CST6CDT", + "SystemV/EST5", + "SystemV/EST5EDT", + "SystemV/HST10", + "SystemV/MST7", + "SystemV/MST7MDT", + "SystemV/PST8", + "SystemV/PST8PDT", + "SystemV/YST9", + "SystemV/YST9YDT", +}; + +} // namespace timezone +} // namespace js + +#endif /* builtin_intl_TimeZoneDataGenerated_h */ diff --git a/js/src/builtin/intl/make_intl_data.py b/js/src/builtin/intl/make_intl_data.py new file mode 100755 index 0000000000..ff631ce219 --- /dev/null +++ b/js/src/builtin/intl/make_intl_data.py @@ -0,0 +1,4139 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# 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/. + +""" Usage: + make_intl_data.py langtags [cldr_common.zip] + make_intl_data.py tzdata + make_intl_data.py currency + make_intl_data.py units + make_intl_data.py numbering + + + Target "langtags": + This script extracts information about 1) mappings between deprecated and + current Unicode BCP 47 locale identifiers, and 2) deprecated and current + BCP 47 Unicode extension value from CLDR, and converts it to C++ mapping + code in intl/components/LocaleGenerated.cpp. The code is used in + intl/components/Locale.cpp. + + + Target "tzdata": + This script computes which time zone informations are not up-to-date in ICU + and provides the necessary mappings to workaround this problem. + https://ssl.icu-project.org/trac/ticket/12044 + + + Target "currency": + Generates the mapping from currency codes to decimal digits used for them. + + + Target "units": + Generate source and test files using the list of so-called "sanctioned unit + identifiers" and verifies that the ICU data filter includes these units. + + + Target "numbering": + Generate source and test files using the list of numbering systems with + simple digit mappings and verifies that it's in sync with ICU/CLDR. +""" + +import io +import json +import os +import re +import sys +import tarfile +import tempfile +from contextlib import closing +from functools import partial, total_ordering +from itertools import chain, groupby, tee +from operator import attrgetter, itemgetter +from zipfile import ZipFile + +import yaml + +if sys.version_info.major == 2: + from itertools import ifilter as filter + from itertools import ifilterfalse as filterfalse + from itertools import imap as map + from itertools import izip_longest as zip_longest + + from urllib2 import Request as UrlRequest + from urllib2 import urlopen + from urlparse import urlsplit +else: + from itertools import filterfalse, zip_longest + from urllib.parse import urlsplit + from urllib.request import Request as UrlRequest + from urllib.request import urlopen + + +# From https://docs.python.org/3/library/itertools.html +def grouper(iterable, n, fillvalue=None): + "Collect data into fixed-length chunks or blocks" + # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx" + args = [iter(iterable)] * n + return zip_longest(*args, fillvalue=fillvalue) + + +def writeMappingHeader(println, description, source, url): + if type(description) is not list: + description = [description] + for desc in description: + println("// {0}".format(desc)) + println("// Derived from {0}.".format(source)) + println("// {0}".format(url)) + + +def writeMappingsVar(println, mapping, name, description, source, url): + """Writes a variable definition with a mapping table. + + Writes the contents of dictionary |mapping| through the |println| + function with the given variable name and a comment with description, + fileDate, and URL. + """ + println("") + writeMappingHeader(println, description, source, url) + println("var {0} = {{".format(name)) + for (key, value) in sorted(mapping.items(), key=itemgetter(0)): + println(' "{0}": "{1}",'.format(key, value)) + println("};") + + +def writeMappingsBinarySearch( + println, + fn_name, + type_name, + name, + validate_fn, + validate_case_fn, + mappings, + tag_maxlength, + description, + source, + url, +): + """Emit code to perform a binary search on language tag subtags. + + Uses the contents of |mapping|, which can either be a dictionary or set, + to emit a mapping function to find subtag replacements. + """ + println("") + writeMappingHeader(println, description, source, url) + println( + """ +bool mozilla::intl::Locale::{0}({1} {2}) {{ + MOZ_ASSERT({3}({2}.Span())); + MOZ_ASSERT({4}({2}.Span())); +""".format( + fn_name, type_name, name, validate_fn, validate_case_fn + ).strip() + ) + writeMappingsBinarySearchBody(println, name, name, mappings, tag_maxlength) + + println( + """ +}""".lstrip( + "\n" + ) + ) + + +def writeMappingsBinarySearchBody( + println, source_name, target_name, mappings, tag_maxlength +): + def write_array(subtags, name, length, fixed): + if fixed: + println( + " static const char {}[{}][{}] = {{".format( + name, len(subtags), length + 1 + ) + ) + else: + println(" static const char* {}[{}] = {{".format(name, len(subtags))) + + # Group in pairs of ten to not exceed the 80 line column limit. + for entries in grouper(subtags, 10): + entries = ( + '"{}"'.format(tag).rjust(length + 2) + for tag in entries + if tag is not None + ) + println(" {},".format(", ".join(entries))) + + println(" };") + + trailing_return = True + + # Sort the subtags by length. That enables using an optimized comparator + # for the binary search, which only performs a single |memcmp| for multiple + # of two subtag lengths. + mappings_keys = mappings.keys() if type(mappings) == dict else mappings + for (length, subtags) in groupby(sorted(mappings_keys, key=len), len): + # Omit the length check if the current length is the maximum length. + if length != tag_maxlength: + println( + """ + if ({}.Length() == {}) {{ +""".format( + source_name, length + ).rstrip( + "\n" + ) + ) + else: + trailing_return = False + println( + """ + { +""".rstrip( + "\n" + ) + ) + + # The subtags need to be sorted for binary search to work. + subtags = sorted(subtags) + + def equals(subtag): + return """{}.EqualTo("{}")""".format(source_name, subtag) + + # Don't emit a binary search for short lists. + if len(subtags) == 1: + if type(mappings) == dict: + println( + """ + if ({}) {{ + {}.Set(mozilla::MakeStringSpan("{}")); + return true; + }} + return false; +""".format( + equals(subtags[0]), target_name, mappings[subtags[0]] + ).strip( + "\n" + ) + ) + else: + println( + """ + return {}; +""".format( + equals(subtags[0]) + ).strip( + "\n" + ) + ) + elif len(subtags) <= 4: + if type(mappings) == dict: + for subtag in subtags: + println( + """ + if ({}) {{ + {}.Set("{}"); + return true; + }} +""".format( + equals(subtag), target_name, mappings[subtag] + ).strip( + "\n" + ) + ) + + println( + """ + return false; +""".strip( + "\n" + ) + ) + else: + cond = (equals(subtag) for subtag in subtags) + cond = (" ||\n" + " " * (4 + len("return "))).join(cond) + println( + """ + return {}; +""".format( + cond + ).strip( + "\n" + ) + ) + else: + write_array(subtags, source_name + "s", length, True) + + if type(mappings) == dict: + write_array([mappings[k] for k in subtags], "aliases", length, False) + + println( + """ + if (const char* replacement = SearchReplacement({0}s, aliases, {0})) {{ + {1}.Set(mozilla::MakeStringSpan(replacement)); + return true; + }} + return false; +""".format( + source_name, target_name + ).rstrip() + ) + else: + println( + """ + return HasReplacement({0}s, {0}); +""".format( + source_name + ).rstrip() + ) + + println( + """ + } +""".strip( + "\n" + ) + ) + + if trailing_return: + println( + """ + return false;""" + ) + + +def writeComplexLanguageTagMappings( + println, complex_language_mappings, description, source, url +): + println("") + writeMappingHeader(println, description, source, url) + println( + """ +void mozilla::intl::Locale::PerformComplexLanguageMappings() { + MOZ_ASSERT(IsStructurallyValidLanguageTag(Language().Span())); + MOZ_ASSERT(IsCanonicallyCasedLanguageTag(Language().Span())); +""".lstrip() + ) + + # Merge duplicate language entries. + language_aliases = {} + for (deprecated_language, (language, script, region)) in sorted( + complex_language_mappings.items(), key=itemgetter(0) + ): + key = (language, script, region) + if key not in language_aliases: + language_aliases[key] = [] + else: + language_aliases[key].append(deprecated_language) + + first_language = True + for (deprecated_language, (language, script, region)) in sorted( + complex_language_mappings.items(), key=itemgetter(0) + ): + key = (language, script, region) + if deprecated_language in language_aliases[key]: + continue + + if_kind = "if" if first_language else "else if" + first_language = False + + cond = ( + 'Language().EqualTo("{}")'.format(lang) + for lang in [deprecated_language] + language_aliases[key] + ) + cond = (" ||\n" + " " * (2 + len(if_kind) + 2)).join(cond) + + println( + """ + {} ({}) {{""".format( + if_kind, cond + ).strip( + "\n" + ) + ) + + println( + """ + SetLanguage("{}");""".format( + language + ).strip( + "\n" + ) + ) + + if script is not None: + println( + """ + if (Script().Missing()) {{ + SetScript("{}"); + }}""".format( + script + ).strip( + "\n" + ) + ) + if region is not None: + println( + """ + if (Region().Missing()) {{ + SetRegion("{}"); + }}""".format( + region + ).strip( + "\n" + ) + ) + println( + """ + }""".strip( + "\n" + ) + ) + + println( + """ +} +""".strip( + "\n" + ) + ) + + +def writeComplexRegionTagMappings( + println, complex_region_mappings, description, source, url +): + println("") + writeMappingHeader(println, description, source, url) + println( + """ +void mozilla::intl::Locale::PerformComplexRegionMappings() { + MOZ_ASSERT(IsStructurallyValidLanguageTag(Language().Span())); + MOZ_ASSERT(IsCanonicallyCasedLanguageTag(Language().Span())); + MOZ_ASSERT(IsStructurallyValidRegionTag(Region().Span())); + MOZ_ASSERT(IsCanonicallyCasedRegionTag(Region().Span())); +""".lstrip() + ) + + # |non_default_replacements| is a list and hence not hashable. Convert it + # to a string to get a proper hashable value. + def hash_key(default, non_default_replacements): + return (default, str(sorted(str(v) for v in non_default_replacements))) + + # Merge duplicate region entries. + region_aliases = {} + for (deprecated_region, (default, non_default_replacements)) in sorted( + complex_region_mappings.items(), key=itemgetter(0) + ): + key = hash_key(default, non_default_replacements) + if key not in region_aliases: + region_aliases[key] = [] + else: + region_aliases[key].append(deprecated_region) + + first_region = True + for (deprecated_region, (default, non_default_replacements)) in sorted( + complex_region_mappings.items(), key=itemgetter(0) + ): + key = hash_key(default, non_default_replacements) + if deprecated_region in region_aliases[key]: + continue + + if_kind = "if" if first_region else "else if" + first_region = False + + cond = ( + 'Region().EqualTo("{}")'.format(region) + for region in [deprecated_region] + region_aliases[key] + ) + cond = (" ||\n" + " " * (2 + len(if_kind) + 2)).join(cond) + + println( + """ + {} ({}) {{""".format( + if_kind, cond + ).strip( + "\n" + ) + ) + + replacement_regions = sorted( + {region for (_, _, region) in non_default_replacements} + ) + + first_case = True + for replacement_region in replacement_regions: + replacement_language_script = sorted( + (language, script) + for (language, script, region) in (non_default_replacements) + if region == replacement_region + ) + + if_kind = "if" if first_case else "else if" + first_case = False + + def compare_tags(language, script): + if script is None: + return 'Language().EqualTo("{}")'.format(language) + return '(Language().EqualTo("{}") && Script().EqualTo("{}"))'.format( + language, script + ) + + cond = ( + compare_tags(language, script) + for (language, script) in replacement_language_script + ) + cond = (" ||\n" + " " * (4 + len(if_kind) + 2)).join(cond) + + println( + """ + {} ({}) {{ + SetRegion("{}"); + }}""".format( + if_kind, cond, replacement_region + ) + .rstrip() + .strip("\n") + ) + + println( + """ + else {{ + SetRegion("{}"); + }} + }}""".format( + default + ) + .rstrip() + .strip("\n") + ) + + println( + """ +} +""".strip( + "\n" + ) + ) + + +def writeVariantTagMappings(println, variant_mappings, description, source, url): + """Writes a function definition that maps variant subtags.""" + println( + """ +static const char* ToCharPointer(const char* str) { + return str; +} + +static const char* ToCharPointer(const mozilla::intl::UniqueChars& str) { + return str.get(); +} + +template <typename T, typename U = T> +static bool IsLessThan(const T& a, const U& b) { + return strcmp(ToCharPointer(a), ToCharPointer(b)) < 0; +} +""" + ) + writeMappingHeader(println, description, source, url) + println( + """ +bool mozilla::intl::Locale::PerformVariantMappings() { + // The variant subtags need to be sorted for binary search. + MOZ_ASSERT(std::is_sorted(mVariants.begin(), mVariants.end(), + IsLessThan<decltype(mVariants)::ElementType>)); + + auto removeVariantAt = [&](size_t index) { + mVariants.erase(mVariants.begin() + index); + }; + + auto insertVariantSortedIfNotPresent = [&](const char* variant) { + auto* p = std::lower_bound( + mVariants.begin(), mVariants.end(), variant, + IsLessThan<decltype(mVariants)::ElementType, decltype(variant)>); + + // Don't insert the replacement when already present. + if (p != mVariants.end() && strcmp(p->get(), variant) == 0) { + return true; + } + + // Insert the preferred variant in sort order. + auto preferred = DuplicateStringToUniqueChars(variant); + return !!mVariants.insert(p, std::move(preferred)); + }; + + for (size_t i = 0; i < mVariants.length();) { + const char* variant = mVariants[i].get(); + MOZ_ASSERT(IsCanonicallyCasedVariantTag(mozilla::MakeStringSpan(variant))); +""".lstrip() + ) + + (no_alias, with_alias) = partition( + variant_mappings.items(), lambda item: item[1] is None + ) + + no_replacements = " ||\n ".join( + f"""strcmp(variant, "{deprecated_variant}") == 0""" + for (deprecated_variant, _) in sorted(no_alias, key=itemgetter(0)) + ) + + println( + f""" + if ({no_replacements}) {{ + removeVariantAt(i); + }} +""".strip( + "\n" + ) + ) + + for (deprecated_variant, (type, replacement)) in sorted( + with_alias, key=itemgetter(0) + ): + println( + f""" + else if (strcmp(variant, "{deprecated_variant}") == 0) {{ + removeVariantAt(i); +""".strip( + "\n" + ) + ) + + if type == "language": + println( + f""" + SetLanguage("{replacement}"); +""".strip( + "\n" + ) + ) + elif type == "region": + println( + f""" + SetRegion("{replacement}"); +""".strip( + "\n" + ) + ) + else: + assert type == "variant" + println( + f""" + if (!insertVariantSortedIfNotPresent("{replacement}")) {{ + return false; + }} +""".strip( + "\n" + ) + ) + + println( + """ + } +""".strip( + "\n" + ) + ) + + println( + """ + else { + i++; + } + } + return true; +} +""".strip( + "\n" + ) + ) + + +def writeLegacyMappingsFunction(println, legacy_mappings, description, source, url): + """Writes a function definition that maps legacy language tags.""" + println("") + writeMappingHeader(println, description, source, url) + println( + """\ +bool mozilla::intl::Locale::UpdateLegacyMappings() { + // We're mapping legacy tags to non-legacy form here. + // Other tags remain unchanged. + // + // Legacy tags are either sign language tags ("sgn") or have one or multiple + // variant subtags. Therefore we can quickly exclude most tags by checking + // these two subtags. + + MOZ_ASSERT(IsCanonicallyCasedLanguageTag(Language().Span())); + + if (!Language().EqualTo("sgn") && mVariants.length() == 0) { + return true; + } + +#ifdef DEBUG + for (const auto& variant : Variants()) { + MOZ_ASSERT(IsStructurallyValidVariantTag(variant)); + MOZ_ASSERT(IsCanonicallyCasedVariantTag(variant)); + } +#endif + + // The variant subtags need to be sorted for binary search. + MOZ_ASSERT(std::is_sorted(mVariants.begin(), mVariants.end(), + IsLessThan<decltype(mVariants)::ElementType>)); + + auto findVariant = [this](const char* variant) { + auto* p = std::lower_bound(mVariants.begin(), mVariants.end(), variant, + IsLessThan<decltype(mVariants)::ElementType, + decltype(variant)>); + + if (p != mVariants.end() && strcmp(p->get(), variant) == 0) { + return p; + } + return static_cast<decltype(p)>(nullptr); + }; + + auto insertVariantSortedIfNotPresent = [&](const char* variant) { + auto* p = std::lower_bound(mVariants.begin(), mVariants.end(), variant, + IsLessThan<decltype(mVariants)::ElementType, + decltype(variant)>); + + // Don't insert the replacement when already present. + if (p != mVariants.end() && strcmp(p->get(), variant) == 0) { + return true; + } + + // Insert the preferred variant in sort order. + auto preferred = DuplicateStringToUniqueChars(variant); + return !!mVariants.insert(p, std::move(preferred)); + }; + + auto removeVariant = [&](auto* p) { + size_t index = std::distance(mVariants.begin(), p); + mVariants.erase(mVariants.begin() + index); + }; + + auto removeVariants = [&](auto* p, auto* q) { + size_t pIndex = std::distance(mVariants.begin(), p); + size_t qIndex = std::distance(mVariants.begin(), q); + MOZ_ASSERT(pIndex < qIndex, "variant subtags are sorted"); + + mVariants.erase(mVariants.begin() + qIndex); + mVariants.erase(mVariants.begin() + pIndex); + };""" + ) + + # Helper class for pattern matching. + class AnyClass: + def __eq__(self, obj): + return obj is not None + + Any = AnyClass() + + # Group the mappings by language. + legacy_mappings_by_language = {} + for (type, replacement) in legacy_mappings.items(): + (language, _, _, _) = type + legacy_mappings_by_language.setdefault(language, {})[type] = replacement + + # Handle the empty language case first. + if None in legacy_mappings_by_language: + # Get the mappings and remove them from the dict. + mappings = legacy_mappings_by_language.pop(None) + + # This case only applies for the "hepburn-heploc" -> "alalc97" + # mapping, so just inline it here. + from_tag = (None, None, None, "hepburn-heploc") + to_tag = (None, None, None, "alalc97") + + assert len(mappings) == 1 + assert mappings[from_tag] == to_tag + + println( + """ + if (mVariants.length() >= 2) { + if (auto* hepburn = findVariant("hepburn")) { + if (auto* heploc = findVariant("heploc")) { + removeVariants(hepburn, heploc); + + if (!insertVariantSortedIfNotPresent("alalc97")) { + return false; + } + } + } + } +""" + ) + + # Handle sign languages next. + if "sgn" in legacy_mappings_by_language: + mappings = legacy_mappings_by_language.pop("sgn") + + # Legacy sign language mappings have the form "sgn-XX" where "XX" is + # some region code. + assert all(type == ("sgn", None, Any, None) for type in mappings.keys()) + + # Legacy sign languages are mapped to a single language subtag. + assert all( + replacement == (Any, None, None, None) for replacement in mappings.values() + ) + + println( + """ + if (Language().EqualTo("sgn")) { + if (Region().Present() && SignLanguageMapping(mLanguage, Region())) { + mRegion.Set(mozilla::MakeStringSpan("")); + } + } +""".rstrip().lstrip( + "\n" + ) + ) + + # Finally handle all remaining cases. + + # The remaining mappings have neither script nor region subtags in the source locale. + assert all( + type == (Any, None, None, Any) + for mappings in legacy_mappings_by_language.values() + for type in mappings.keys() + ) + + # And they have neither script nor region nor variant subtags in the target locale. + assert all( + replacement == (Any, None, None, None) + for mappings in legacy_mappings_by_language.values() + for replacement in mappings.values() + ) + + # Compact the mappings table by removing empty fields. + legacy_mappings_by_language = { + lang: { + variants: r_language + for ((_, _, _, variants), (r_language, _, _, _)) in mappings.items() + } + for (lang, mappings) in legacy_mappings_by_language.items() + } + + # Try to combine the remaining cases. + legacy_mappings_compact = {} + + # Python can't hash dicts or lists, so use the string representation as the hash key. + def hash_key(mappings): + return str(sorted(mappings.items(), key=itemgetter(0))) + + for (lang, mappings) in sorted( + legacy_mappings_by_language.items(), key=itemgetter(0) + ): + key = hash_key(mappings) + legacy_mappings_compact.setdefault(key, []).append(lang) + + for langs in legacy_mappings_compact.values(): + language_equal_to = ( + f"""Language().EqualTo("{lang}")""" for lang in sorted(langs) + ) + cond = f""" ||\n{" " * len(" else if (")}""".join(language_equal_to) + + println( + f""" + else if ({cond}) {{ +""".rstrip().lstrip( + "\n" + ) + ) + + mappings = legacy_mappings_by_language[langs[0]] + + # Count the variant subtags to determine the sort order. + def variant_size(m): + (k, _) = m + return len(k.split("-")) + + # Alias rules are applied by largest union size first. + for (size, mappings_by_size) in groupby( + sorted(mappings.items(), key=variant_size, reverse=True), key=variant_size + ): + + # Convert grouper object to dict. + mappings_by_size = dict(mappings_by_size) + + is_first = True + chain_if = size == 1 + + # Alias rules are applied in alphabetical order + for (variants, r_language) in sorted( + mappings_by_size.items(), key=itemgetter(0) + ): + sorted_variants = sorted(variants.split("-")) + len_variants = len(sorted_variants) + + maybe_else = "else " if chain_if and not is_first else "" + is_first = False + + for (i, variant) in enumerate(sorted_variants): + println( + f""" + {" " * i}{maybe_else}if (auto* {variant} = findVariant("{variant}")) {{ +""".rstrip().lstrip( + "\n" + ) + ) + + indent = " " * len_variants + + println( + f""" + {indent}removeVariant{"s" if len_variants > 1 else ""}({", ".join(sorted_variants)}); + {indent}SetLanguage("{r_language}"); + {indent}{"return true;" if not chain_if else ""} +""".rstrip().lstrip( + "\n" + ) + ) + + for i in range(len_variants, 0, -1): + println( + f""" + {" " * (i - 1)}}} +""".rstrip().lstrip( + "\n" + ) + ) + + println( + """ + } +""".rstrip().lstrip( + "\n" + ) + ) + + println( + """ + return true; +}""" + ) + + +def writeSignLanguageMappingsFunction( + println, legacy_mappings, description, source, url +): + """Writes a function definition that maps legacy sign language tags.""" + println("") + writeMappingHeader(println, description, source, url) + println( + """\ +bool mozilla::intl::Locale::SignLanguageMapping(LanguageSubtag& language, + const RegionSubtag& region) { + MOZ_ASSERT(language.EqualTo("sgn")); + MOZ_ASSERT(IsStructurallyValidRegionTag(region.Span())); + MOZ_ASSERT(IsCanonicallyCasedRegionTag(region.Span())); +""".rstrip() + ) + + region_mappings = { + rg: lg + for ((lang, _, rg, _), (lg, _, _, _)) in legacy_mappings.items() + if lang == "sgn" + } + + source_name = "region" + target_name = "language" + tag_maxlength = 3 + writeMappingsBinarySearchBody( + println, source_name, target_name, region_mappings, tag_maxlength + ) + + println( + """ +}""".lstrip() + ) + + +def readSupplementalData(core_file): + """Reads CLDR Supplemental Data and extracts information for Intl.js. + + Information extracted: + - legacyMappings: mappings from legacy tags to preferred complete language tags + - languageMappings: mappings from language subtags to preferred subtags + - complexLanguageMappings: mappings from language subtags with complex rules + - regionMappings: mappings from region subtags to preferred subtags + - complexRegionMappings: mappings from region subtags with complex rules + - variantMappings: mappings from variant subtags to preferred subtags + - likelySubtags: likely subtags used for generating test data only + Returns these mappings as dictionaries. + """ + import xml.etree.ElementTree as ET + + # From Unicode BCP 47 locale identifier <https://unicode.org/reports/tr35/>. + re_unicode_language_id = re.compile( + r""" + ^ + # unicode_language_id = unicode_language_subtag + # unicode_language_subtag = alpha{2,3} | alpha{5,8} + (?P<language>[a-z]{2,3}|[a-z]{5,8}) + + # (sep unicode_script_subtag)? + # unicode_script_subtag = alpha{4} + (?:-(?P<script>[a-z]{4}))? + + # (sep unicode_region_subtag)? + # unicode_region_subtag = (alpha{2} | digit{3}) + (?:-(?P<region>([a-z]{2}|[0-9]{3})))? + + # (sep unicode_variant_subtag)* + # unicode_variant_subtag = (alphanum{5,8} | digit alphanum{3}) + (?P<variants>(-([a-z0-9]{5,8}|[0-9][a-z0-9]{3}))+)? + $ + """, + re.IGNORECASE | re.VERBOSE, + ) + + # CLDR uses "_" as the separator for some elements. Replace it with "-". + def bcp47_id(cldr_id): + return cldr_id.replace("_", "-") + + # Return the tuple (language, script, region, variants) and assert all + # subtags are in canonical case. + def bcp47_canonical(language, script, region, variants): + # Canonical case for language subtags is lower case. + assert language is None or language.lower() == language + + # Canonical case for script subtags is title case. + assert script is None or script.title() == script + + # Canonical case for region subtags is upper case. + assert region is None or region.upper() == region + + # Canonical case for variant subtags is lower case. + assert variants is None or variants.lower() == variants + + return (language, script, region, variants[1:] if variants else None) + + # Language ids are interpreted as multi-maps in + # <https://www.unicode.org/reports/tr35/#LocaleId_Canonicalization>. + # + # See UTS35, §Annex C, Definitions - 1. Multimap interpretation. + def language_id_to_multimap(language_id): + match = re_unicode_language_id.match(language_id) + assert ( + match is not None + ), f"{language_id} invalid Unicode BCP 47 locale identifier" + + canonical_language_id = bcp47_canonical( + *match.group("language", "script", "region", "variants") + ) + (language, _, _, _) = canonical_language_id + + # Normalize "und" language to None, but keep the rest as is. + return (language if language != "und" else None,) + canonical_language_id[1:] + + rules = {} + territory_exception_rules = {} + + tree = ET.parse(core_file.open("common/supplemental/supplementalMetadata.xml")) + + # Load the rules from supplementalMetadata.xml. + # + # See UTS35, §Annex C, Definitions - 2. Alias elements. + # See UTS35, §Annex C, Preprocessing. + for alias_name in [ + "languageAlias", + "scriptAlias", + "territoryAlias", + "variantAlias", + ]: + for alias in tree.iterfind(".//" + alias_name): + # Replace '_' by '-'. + type = bcp47_id(alias.get("type")) + replacement = bcp47_id(alias.get("replacement")) + + # Prefix with "und-". + if alias_name != "languageAlias": + type = "und-" + type + + # Discard all rules where the type is an invalid languageId. + if re_unicode_language_id.match(type) is None: + continue + + type = language_id_to_multimap(type) + + # Multiple, whitespace-separated territory replacements may be present. + if alias_name == "territoryAlias" and " " in replacement: + replacements = replacement.split(" ") + replacement_list = [ + language_id_to_multimap("und-" + r) for r in replacements + ] + + assert ( + type not in territory_exception_rules + ), f"Duplicate alias rule: {type}" + + territory_exception_rules[type] = replacement_list + + # The first element is the default territory replacement. + replacement = replacements[0] + + # Prefix with "und-". + if alias_name != "languageAlias": + replacement = "und-" + replacement + + replacement = language_id_to_multimap(replacement) + + assert type not in rules, f"Duplicate alias rule: {type}" + + rules[type] = replacement + + # Helper class for pattern matching. + class AnyClass: + def __eq__(self, obj): + return obj is not None + + Any = AnyClass() + + modified_rules = True + loop_count = 0 + + while modified_rules: + modified_rules = False + loop_count += 1 + + # UTS 35 defines that canonicalization is applied until a fixed point has + # been reached. This iterative application of the canonicalization algorithm + # is only needed for a relatively small set of rules, so we can precompute + # the transitive closure of all rules here and then perform a single pass + # when canonicalizing language tags at runtime. + transitive_rules = {} + + # Compute the transitive closure. + # Any case which currently doesn't occur in the CLDR sources isn't supported + # and will lead to throwing an error. + for (type, replacement) in rules.items(): + (language, script, region, variants) = type + (r_language, r_script, r_region, r_variants) = replacement + + for (i_type, i_replacement) in rules.items(): + (i_language, i_script, i_region, i_variants) = i_type + (i_r_language, i_r_script, i_r_region, i_r_variants) = i_replacement + + if i_language is not None and i_language == r_language: + # This case currently only occurs when neither script nor region + # subtags are present. A single variant subtags may be present + # in |type|. And |i_type| definitely has a single variant subtag. + # Should this ever change, update this code accordingly. + assert type == (Any, None, None, None) or type == ( + Any, + None, + None, + Any, + ) + assert replacement == (Any, None, None, None) + assert i_type == (Any, None, None, Any) + assert i_replacement == (Any, None, None, None) + + # This case happens for the rules + # "zh-guoyu -> zh", + # "zh-hakka -> hak", and + # "und-hakka -> und". + # Given the possible input "zh-guoyu-hakka", the first rule will + # change it to "zh-hakka", and then the second rule can be + # applied. (The third rule isn't applied ever.) + # + # Let's assume there's a hypothetical rule + # "zh-aaaaa" -> "en" + # And we have the input "zh-aaaaa-hakka", then "zh-aaaaa -> en" + # is applied before "zh-hakka -> hak", because rules are sorted + # alphabetically. That means the overall result is "en": + # "zh-aaaaa-hakka" is first canonicalized to "en-hakka" and then + # "hakka" is removed through the third rule. + # + # No current rule requires to handle this special case, so we + # don't yet support it. + assert variants is None or variants <= i_variants + + # Combine all variants and remove duplicates. + vars = set( + i_variants.split("-") + + (variants.split("-") if variants else []) + ) + + # Add the variants alphabetically sorted. + n_type = (language, None, None, "-".join(sorted(vars))) + + assert ( + n_type not in transitive_rules + or transitive_rules[n_type] == i_replacement + ) + transitive_rules[n_type] = i_replacement + + continue + + if i_script is not None and i_script == r_script: + # This case currently doesn't occur, so we don't yet support it. + raise ValueError( + f"{type} -> {replacement} :: {i_type} -> {i_replacement}" + ) + if i_region is not None and i_region == r_region: + # This case currently only applies for sign language + # replacements. Similar to the language subtag case any other + # combination isn't currently supported. + assert type == (None, None, Any, None) + assert replacement == (None, None, Any, None) + assert i_type == ("sgn", None, Any, None) + assert i_replacement == (Any, None, None, None) + + n_type = ("sgn", None, region, None) + + assert n_type not in transitive_rules + transitive_rules[n_type] = i_replacement + + continue + + if i_variants is not None and i_variants == r_variants: + # This case currently doesn't occur, so we don't yet support it. + raise ValueError( + f"{type} -> {replacement} :: {i_type} -> {i_replacement}" + ) + + # Ensure there are no contradicting rules. + assert all( + rules[type] == replacement + for (type, replacement) in transitive_rules.items() + if type in rules + ) + + # If |transitive_rules| is not a subset of |rules|, new rules will be added. + modified_rules = not (transitive_rules.keys() <= rules.keys()) + + # Ensure we only have to iterate more than once for the "guoyo-{hakka,xiang}" + # case. Failing this assertion means either there's a bug when computing the + # stop condition of this loop or a new kind of legacy language tags was added. + if modified_rules and loop_count > 1: + new_rules = {k for k in transitive_rules.keys() if k not in rules} + for k in new_rules: + assert k == (Any, None, None, "guoyu-hakka") or k == ( + Any, + None, + None, + "guoyu-xiang", + ) + + # Merge the transitive rules. + rules.update(transitive_rules) + + # Computes the size of the union of all field value sets. + def multi_map_size(locale_id): + (language, script, region, variants) = locale_id + + return ( + (1 if language is not None else 0) + + (1 if script is not None else 0) + + (1 if region is not None else 0) + + (len(variants.split("-")) if variants is not None else 0) + ) + + # Dictionary of legacy mappings, contains raw rules, e.g. + # (None, None, None, "hepburn-heploc") -> (None, None, None, "alalc97"). + legacy_mappings = {} + + # Dictionary of simple language subtag mappings, e.g. "in" -> "id". + language_mappings = {} + + # Dictionary of complex language subtag mappings, modifying more than one + # subtag, e.g. "sh" -> ("sr", "Latn", None) and "cnr" -> ("sr", None, "ME"). + complex_language_mappings = {} + + # Dictionary of simple script subtag mappings, e.g. "Qaai" -> "Zinh". + script_mappings = {} + + # Dictionary of simple region subtag mappings, e.g. "DD" -> "DE". + region_mappings = {} + + # Dictionary of complex region subtag mappings, containing more than one + # replacement, e.g. "SU" -> ("RU", ["AM", "AZ", "BY", ...]). + complex_region_mappings = {} + + # Dictionary of aliased variant subtags to a tuple of preferred replacement + # type and replacement, e.g. "arevela" -> ("language", "hy") or + # "aaland" -> ("region", "AX") or "heploc" -> ("variant", "alalc97"). + variant_mappings = {} + + # Preprocess all rules so we can perform a single lookup per subtag at runtime. + for (type, replacement) in rules.items(): + (language, script, region, variants) = type + (r_language, r_script, r_region, r_variants) = replacement + + type_map_size = multi_map_size(type) + + # Most mappings are one-to-one and can be encoded through lookup tables. + if type_map_size == 1: + if language is not None: + assert r_language is not None, "Can't remove a language subtag" + + # We don't yet support this case. + assert ( + r_variants is None + ), f"Unhandled variant replacement in language alias: {replacement}" + + if replacement == (Any, None, None, None): + language_mappings[language] = r_language + else: + complex_language_mappings[language] = replacement[:-1] + elif script is not None: + # We don't support removing script subtags. + assert ( + r_script is not None + ), f"Can't remove a script subtag: {replacement}" + + # We only support one-to-one script mappings for now. + assert replacement == ( + None, + Any, + None, + None, + ), f"Unhandled replacement in script alias: {replacement}" + + script_mappings[script] = r_script + elif region is not None: + # We don't support removing region subtags. + assert ( + r_region is not None + ), f"Can't remove a region subtag: {replacement}" + + # We only support one-to-one region mappings for now. + assert replacement == ( + None, + None, + Any, + None, + ), f"Unhandled replacement in region alias: {replacement}" + + if type not in territory_exception_rules: + region_mappings[region] = r_region + else: + complex_region_mappings[region] = [ + r_region + for (_, _, r_region, _) in territory_exception_rules[type] + ] + else: + assert variants is not None + assert len(variants.split("-")) == 1 + + # We only support one-to-one variant mappings for now. + assert ( + multi_map_size(replacement) <= 1 + ), f"Unhandled replacement in variant alias: {replacement}" + + if r_language is not None: + variant_mappings[variants] = ("language", r_language) + elif r_script is not None: + variant_mappings[variants] = ("script", r_script) + elif r_region is not None: + variant_mappings[variants] = ("region", r_region) + elif r_variants is not None: + assert len(r_variants.split("-")) == 1 + variant_mappings[variants] = ("variant", r_variants) + else: + variant_mappings[variants] = None + else: + # Alias rules which have multiple input fields must be processed + # first. This applies only to a handful of rules, so our generated + # code adds fast paths to skip these rules in the common case. + + # Case 1: Language and at least one variant subtag. + if language is not None and variants is not None: + pass + + # Case 2: Sign language and a region subtag. + elif language == "sgn" and region is not None: + pass + + # Case 3: "hepburn-heploc" to "alalc97" canonicalization. + elif ( + language is None + and variants is not None + and len(variants.split("-")) == 2 + ): + pass + + # Any other combination is currently unsupported. + else: + raise ValueError(f"{type} -> {replacement}") + + legacy_mappings[type] = replacement + + tree = ET.parse(core_file.open("common/supplemental/likelySubtags.xml")) + + likely_subtags = {} + + for likely_subtag in tree.iterfind(".//likelySubtag"): + from_tag = bcp47_id(likely_subtag.get("from")) + from_match = re_unicode_language_id.match(from_tag) + assert ( + from_match is not None + ), f"{from_tag} invalid Unicode BCP 47 locale identifier" + assert ( + from_match.group("variants") is None + ), f"unexpected variant subtags in {from_tag}" + + to_tag = bcp47_id(likely_subtag.get("to")) + to_match = re_unicode_language_id.match(to_tag) + assert ( + to_match is not None + ), f"{to_tag} invalid Unicode BCP 47 locale identifier" + assert ( + to_match.group("variants") is None + ), f"unexpected variant subtags in {to_tag}" + + from_canonical = bcp47_canonical( + *from_match.group("language", "script", "region", "variants") + ) + + to_canonical = bcp47_canonical( + *to_match.group("language", "script", "region", "variants") + ) + + # Remove the empty variant subtags. + from_canonical = from_canonical[:-1] + to_canonical = to_canonical[:-1] + + likely_subtags[from_canonical] = to_canonical + + complex_region_mappings_final = {} + + for (deprecated_region, replacements) in complex_region_mappings.items(): + # Find all likely subtag entries which don't already contain a region + # subtag and whose target region is in the list of replacement regions. + region_likely_subtags = [ + (from_language, from_script, to_region) + for ( + (from_language, from_script, from_region), + (_, _, to_region), + ) in likely_subtags.items() + if from_region is None and to_region in replacements + ] + + # The first replacement entry is the default region. + default = replacements[0] + + # Find all likely subtag entries whose region matches the default region. + default_replacements = { + (language, script) + for (language, script, region) in region_likely_subtags + if region == default + } + + # And finally find those entries which don't use the default region. + # These are the entries we're actually interested in, because those need + # to be handled specially when selecting the correct preferred region. + non_default_replacements = [ + (language, script, region) + for (language, script, region) in region_likely_subtags + if (language, script) not in default_replacements + ] + + # Remove redundant mappings. + # + # For example starting with CLDR 43, the deprecated region "SU" has the + # following non-default replacement entries for "GE": + # - ('sva', None, 'GE') + # - ('sva', 'Cyrl', 'GE') + # - ('sva', 'Latn', 'GE') + # + # The latter two entries are redundant, because they're already handled + # by the first entry. + non_default_replacements = [ + (language, script, region) + for (language, script, region) in non_default_replacements + if script is None + or (language, None, region) not in non_default_replacements + ] + + # If there are no non-default replacements, we can handle the region as + # part of the simple region mapping. + if non_default_replacements: + complex_region_mappings_final[deprecated_region] = ( + default, + non_default_replacements, + ) + else: + region_mappings[deprecated_region] = default + + return { + "legacyMappings": legacy_mappings, + "languageMappings": language_mappings, + "complexLanguageMappings": complex_language_mappings, + "scriptMappings": script_mappings, + "regionMappings": region_mappings, + "complexRegionMappings": complex_region_mappings_final, + "variantMappings": variant_mappings, + "likelySubtags": likely_subtags, + } + + +def readUnicodeExtensions(core_file): + import xml.etree.ElementTree as ET + + # Match all xml-files in the BCP 47 directory. + bcpFileRE = re.compile(r"^common/bcp47/.+\.xml$") + + # https://www.unicode.org/reports/tr35/#Unicode_locale_identifier + # + # type = alphanum{3,8} (sep alphanum{3,8})* ; + typeRE = re.compile(r"^[a-z0-9]{3,8}(-[a-z0-9]{3,8})*$") + + # https://www.unicode.org/reports/tr35/#Unicode_language_identifier + # + # unicode_region_subtag = alpha{2} ; + alphaRegionRE = re.compile(r"^[A-Z]{2}$", re.IGNORECASE) + + # Mapping from Unicode extension types to dict of deprecated to + # preferred values. + mapping = { + # Unicode BCP 47 U Extension + "u": {}, + # Unicode BCP 47 T Extension + "t": {}, + } + + def readBCP47File(file): + tree = ET.parse(file) + for keyword in tree.iterfind(".//keyword/key"): + extension = keyword.get("extension", "u") + assert ( + extension == "u" or extension == "t" + ), "unknown extension type: {}".format(extension) + + extension_name = keyword.get("name") + + for type in keyword.iterfind("type"): + # <https://unicode.org/reports/tr35/#Unicode_Locale_Extension_Data_Files>: + # + # The key or type name used by Unicode locale extension with 'u' extension + # syntax or the 't' extensions syntax. When alias below is absent, this name + # can be also used with the old style "@key=type" syntax. + name = type.get("name") + + # Ignore the special name: + # - <https://unicode.org/reports/tr35/#CODEPOINTS> + # - <https://unicode.org/reports/tr35/#REORDER_CODE> + # - <https://unicode.org/reports/tr35/#RG_KEY_VALUE> + # - <https://unicode.org/reports/tr35/#SCRIPT_CODE> + # - <https://unicode.org/reports/tr35/#SUBDIVISION_CODE> + # - <https://unicode.org/reports/tr35/#PRIVATE_USE> + if name in ( + "CODEPOINTS", + "REORDER_CODE", + "RG_KEY_VALUE", + "SCRIPT_CODE", + "SUBDIVISION_CODE", + "PRIVATE_USE", + ): + continue + + # All other names should match the 'type' production. + assert ( + typeRE.match(name) is not None + ), "{} matches the 'type' production".format(name) + + # <https://unicode.org/reports/tr35/#Unicode_Locale_Extension_Data_Files>: + # + # The preferred value of the deprecated key, type or attribute element. + # When a key, type or attribute element is deprecated, this attribute is + # used for specifying a new canonical form if available. + preferred = type.get("preferred") + + # <https://unicode.org/reports/tr35/#Unicode_Locale_Extension_Data_Files>: + # + # The BCP 47 form is the canonical form, and recommended. Other aliases are + # included only for backwards compatibility. + alias = type.get("alias") + + # <https://unicode.org/reports/tr35/#Canonical_Unicode_Locale_Identifiers> + # + # Use the bcp47 data to replace keys, types, tfields, and tvalues by their + # canonical forms. See Section 3.6.4 U Extension Data Files) and Section + # 3.7.1 T Extension Data Files. The aliases are in the alias attribute + # value, while the canonical is in the name attribute value. + + # 'preferred' contains the new preferred name, 'alias' the compatibility + # name, but then there's this entry where 'preferred' and 'alias' are the + # same. So which one to choose? Assume 'preferred' is the actual canonical + # name. + # + # <type name="islamicc" + # description="Civil (algorithmic) Arabic calendar" + # deprecated="true" + # preferred="islamic-civil" + # alias="islamic-civil"/> + + if preferred is not None: + assert typeRE.match(preferred), preferred + mapping[extension].setdefault(extension_name, {})[name] = preferred + + if alias is not None: + for alias_name in alias.lower().split(" "): + # Ignore alias entries which don't match the 'type' production. + if typeRE.match(alias_name) is None: + continue + + # See comment above when 'alias' and 'preferred' are both present. + if ( + preferred is not None + and name in mapping[extension][extension_name] + ): + continue + + # Skip over entries where 'name' and 'alias' are equal. + # + # <type name="pst8pdt" + # description="POSIX style time zone for US Pacific Time" + # alias="PST8PDT" + # since="1.8"/> + if name == alias_name: + continue + + mapping[extension].setdefault(extension_name, {})[ + alias_name + ] = name + + def readSupplementalMetadata(file): + # Find subdivision and region replacements. + # + # <https://www.unicode.org/reports/tr35/#Canonical_Unicode_Locale_Identifiers> + # + # Replace aliases in special key values: + # - If there is an 'sd' or 'rg' key, replace any subdivision alias + # in its value in the same way, using subdivisionAlias data. + tree = ET.parse(file) + for alias in tree.iterfind(".//subdivisionAlias"): + type = alias.get("type") + assert ( + typeRE.match(type) is not None + ), "{} matches the 'type' production".format(type) + + # Take the first replacement when multiple ones are present. + replacement = alias.get("replacement").split(" ")[0].lower() + + # Append "zzzz" if the replacement is a two-letter region code. + if alphaRegionRE.match(replacement) is not None: + replacement += "zzzz" + + # Assert the replacement is syntactically correct. + assert ( + typeRE.match(replacement) is not None + ), "replacement {} matches the 'type' production".format(replacement) + + # 'subdivisionAlias' applies to 'rg' and 'sd' keys. + mapping["u"].setdefault("rg", {})[type] = replacement + mapping["u"].setdefault("sd", {})[type] = replacement + + for name in core_file.namelist(): + if bcpFileRE.match(name): + readBCP47File(core_file.open(name)) + + readSupplementalMetadata( + core_file.open("common/supplemental/supplementalMetadata.xml") + ) + + return { + "unicodeMappings": mapping["u"], + "transformMappings": mapping["t"], + } + + +def writeCLDRLanguageTagData(println, data, url): + """Writes the language tag data to the Intl data file.""" + + println(generatedFileWarning) + println("// Version: CLDR-{}".format(data["version"])) + println("// URL: {}".format(url)) + + println( + """ +#include "mozilla/Assertions.h" +#include "mozilla/Span.h" +#include "mozilla/TextUtils.h" + +#include <algorithm> +#include <cstdint> +#include <cstring> +#include <iterator> +#include <string> +#include <type_traits> + +#include "mozilla/intl/Locale.h" + +using namespace mozilla::intl::LanguageTagLimits; + +template <size_t Length, size_t TagLength, size_t SubtagLength> +static inline bool HasReplacement( + const char (&subtags)[Length][TagLength], + const mozilla::intl::LanguageTagSubtag<SubtagLength>& subtag) { + MOZ_ASSERT(subtag.Length() == TagLength - 1, + "subtag must have the same length as the list of subtags"); + + const char* ptr = subtag.Span().data(); + return std::binary_search(std::begin(subtags), std::end(subtags), ptr, + [](const char* a, const char* b) { + return memcmp(a, b, TagLength - 1) < 0; + }); +} + +template <size_t Length, size_t TagLength, size_t SubtagLength> +static inline const char* SearchReplacement( + const char (&subtags)[Length][TagLength], const char* (&aliases)[Length], + const mozilla::intl::LanguageTagSubtag<SubtagLength>& subtag) { + MOZ_ASSERT(subtag.Length() == TagLength - 1, + "subtag must have the same length as the list of subtags"); + + const char* ptr = subtag.Span().data(); + auto p = std::lower_bound(std::begin(subtags), std::end(subtags), ptr, + [](const char* a, const char* b) { + return memcmp(a, b, TagLength - 1) < 0; + }); + if (p != std::end(subtags) && memcmp(*p, ptr, TagLength - 1) == 0) { + return aliases[std::distance(std::begin(subtags), p)]; + } + return nullptr; +} + +#ifdef DEBUG +static bool IsAsciiLowercaseAlphanumeric(char c) { + return mozilla::IsAsciiLowercaseAlpha(c) || mozilla::IsAsciiDigit(c); +} + +static bool IsAsciiLowercaseAlphanumericOrDash(char c) { + return IsAsciiLowercaseAlphanumeric(c) || c == '-'; +} + +static bool IsCanonicallyCasedLanguageTag(mozilla::Span<const char> span) { + return std::all_of(span.begin(), span.end(), + mozilla::IsAsciiLowercaseAlpha<char>); +} + +static bool IsCanonicallyCasedScriptTag(mozilla::Span<const char> span) { + return mozilla::IsAsciiUppercaseAlpha(span[0]) && + std::all_of(span.begin() + 1, span.end(), + mozilla::IsAsciiLowercaseAlpha<char>); +} + +static bool IsCanonicallyCasedRegionTag(mozilla::Span<const char> span) { + return std::all_of(span.begin(), span.end(), + mozilla::IsAsciiUppercaseAlpha<char>) || + std::all_of(span.begin(), span.end(), mozilla::IsAsciiDigit<char>); +} + +static bool IsCanonicallyCasedVariantTag(mozilla::Span<const char> span) { + return std::all_of(span.begin(), span.end(), IsAsciiLowercaseAlphanumeric); +} + +static bool IsCanonicallyCasedUnicodeKey(mozilla::Span<const char> key) { + return std::all_of(key.begin(), key.end(), IsAsciiLowercaseAlphanumeric); +} + +static bool IsCanonicallyCasedUnicodeType(mozilla::Span<const char> type) { + return std::all_of(type.begin(), type.end(), + IsAsciiLowercaseAlphanumericOrDash); +} + +static bool IsCanonicallyCasedTransformKey(mozilla::Span<const char> key) { + return std::all_of(key.begin(), key.end(), IsAsciiLowercaseAlphanumeric); +} + +static bool IsCanonicallyCasedTransformType(mozilla::Span<const char> type) { + return std::all_of(type.begin(), type.end(), + IsAsciiLowercaseAlphanumericOrDash); +} +#endif +""".rstrip() + ) + + source = "CLDR Supplemental Data, version {}".format(data["version"]) + legacy_mappings = data["legacyMappings"] + language_mappings = data["languageMappings"] + complex_language_mappings = data["complexLanguageMappings"] + script_mappings = data["scriptMappings"] + region_mappings = data["regionMappings"] + complex_region_mappings = data["complexRegionMappings"] + variant_mappings = data["variantMappings"] + unicode_mappings = data["unicodeMappings"] + transform_mappings = data["transformMappings"] + + # unicode_language_subtag = alpha{2,3} | alpha{5,8} ; + language_maxlength = 8 + + # unicode_script_subtag = alpha{4} ; + script_maxlength = 4 + + # unicode_region_subtag = (alpha{2} | digit{3}) ; + region_maxlength = 3 + + writeMappingsBinarySearch( + println, + "LanguageMapping", + "LanguageSubtag&", + "language", + "IsStructurallyValidLanguageTag", + "IsCanonicallyCasedLanguageTag", + language_mappings, + language_maxlength, + "Mappings from language subtags to preferred values.", + source, + url, + ) + writeMappingsBinarySearch( + println, + "ComplexLanguageMapping", + "const LanguageSubtag&", + "language", + "IsStructurallyValidLanguageTag", + "IsCanonicallyCasedLanguageTag", + complex_language_mappings.keys(), + language_maxlength, + "Language subtags with complex mappings.", + source, + url, + ) + writeMappingsBinarySearch( + println, + "ScriptMapping", + "ScriptSubtag&", + "script", + "IsStructurallyValidScriptTag", + "IsCanonicallyCasedScriptTag", + script_mappings, + script_maxlength, + "Mappings from script subtags to preferred values.", + source, + url, + ) + writeMappingsBinarySearch( + println, + "RegionMapping", + "RegionSubtag&", + "region", + "IsStructurallyValidRegionTag", + "IsCanonicallyCasedRegionTag", + region_mappings, + region_maxlength, + "Mappings from region subtags to preferred values.", + source, + url, + ) + writeMappingsBinarySearch( + println, + "ComplexRegionMapping", + "const RegionSubtag&", + "region", + "IsStructurallyValidRegionTag", + "IsCanonicallyCasedRegionTag", + complex_region_mappings.keys(), + region_maxlength, + "Region subtags with complex mappings.", + source, + url, + ) + + writeComplexLanguageTagMappings( + println, + complex_language_mappings, + "Language subtags with complex mappings.", + source, + url, + ) + writeComplexRegionTagMappings( + println, + complex_region_mappings, + "Region subtags with complex mappings.", + source, + url, + ) + + writeVariantTagMappings( + println, + variant_mappings, + "Mappings from variant subtags to preferred values.", + source, + url, + ) + + writeLegacyMappingsFunction( + println, legacy_mappings, "Canonicalize legacy locale identifiers.", source, url + ) + + writeSignLanguageMappingsFunction( + println, legacy_mappings, "Mappings from legacy sign languages.", source, url + ) + + writeUnicodeExtensionsMappings(println, unicode_mappings, "Unicode") + writeUnicodeExtensionsMappings(println, transform_mappings, "Transform") + + +def writeCLDRLanguageTagLikelySubtagsTest(println, data, url): + """Writes the likely-subtags test file.""" + + println(generatedFileWarning) + + source = "CLDR Supplemental Data, version {}".format(data["version"]) + language_mappings = data["languageMappings"] + complex_language_mappings = data["complexLanguageMappings"] + script_mappings = data["scriptMappings"] + region_mappings = data["regionMappings"] + complex_region_mappings = data["complexRegionMappings"] + likely_subtags = data["likelySubtags"] + + def bcp47(tag): + (language, script, region) = tag + return "{}{}{}".format( + language, "-" + script if script else "", "-" + region if region else "" + ) + + def canonical(tag): + (language, script, region) = tag + + # Map deprecated language subtags. + if language in language_mappings: + language = language_mappings[language] + elif language in complex_language_mappings: + (language2, script2, region2) = complex_language_mappings[language] + (language, script, region) = ( + language2, + script if script else script2, + region if region else region2, + ) + + # Map deprecated script subtags. + if script in script_mappings: + script = script_mappings[script] + + # Map deprecated region subtags. + if region in region_mappings: + region = region_mappings[region] + else: + # Assume no complex region mappings are needed for now. + assert ( + region not in complex_region_mappings + ), "unexpected region with complex mappings: {}".format(region) + + return (language, script, region) + + # https://unicode.org/reports/tr35/#Likely_Subtags + + def addLikelySubtags(tag): + # Step 1: Canonicalize. + (language, script, region) = canonical(tag) + if script == "Zzzz": + script = None + if region == "ZZ": + region = None + + # Step 2: Lookup. + searches = ( + (language, script, region), + (language, None, region), + (language, script, None), + (language, None, None), + ("und", script, None), + ) + search = next(search for search in searches if search in likely_subtags) + + (language_s, script_s, region_s) = search + (language_m, script_m, region_m) = likely_subtags[search] + + # Step 3: Return. + return ( + language if language != language_s else language_m, + script if script != script_s else script_m, + region if region != region_s else region_m, + ) + + # https://unicode.org/reports/tr35/#Likely_Subtags + def removeLikelySubtags(tag): + # Step 1: Add likely subtags. + max = addLikelySubtags(tag) + + # Step 2: Remove variants (doesn't apply here). + + # Step 3: Find a match. + (language, script, region) = max + for trial in ( + (language, None, None), + (language, None, region), + (language, script, None), + ): + if addLikelySubtags(trial) == max: + return trial + + # Step 4: Return maximized if no match found. + return max + + def likely_canonical(from_tag, to_tag): + # Canonicalize the input tag. + from_tag = canonical(from_tag) + + # Update the expected result if necessary. + if from_tag in likely_subtags: + to_tag = likely_subtags[from_tag] + + # Canonicalize the expected output. + to_canonical = canonical(to_tag) + + # Sanity check: This should match the result of |addLikelySubtags|. + assert to_canonical == addLikelySubtags(from_tag) + + return to_canonical + + # |likely_subtags| contains non-canonicalized tags, so canonicalize it first. + likely_subtags_canonical = { + k: likely_canonical(k, v) for (k, v) in likely_subtags.items() + } + + # Add test data for |Intl.Locale.prototype.maximize()|. + writeMappingsVar( + println, + {bcp47(k): bcp47(v) for (k, v) in likely_subtags_canonical.items()}, + "maxLikelySubtags", + "Extracted from likelySubtags.xml.", + source, + url, + ) + + # Use the maximalized tags as the input for the remove likely-subtags test. + minimized = { + tag: removeLikelySubtags(tag) for tag in likely_subtags_canonical.values() + } + + # Add test data for |Intl.Locale.prototype.minimize()|. + writeMappingsVar( + println, + {bcp47(k): bcp47(v) for (k, v) in minimized.items()}, + "minLikelySubtags", + "Extracted from likelySubtags.xml.", + source, + url, + ) + + println( + """ +for (let [tag, maximal] of Object.entries(maxLikelySubtags)) { + assertEq(new Intl.Locale(tag).maximize().toString(), maximal); +}""" + ) + + println( + """ +for (let [tag, minimal] of Object.entries(minLikelySubtags)) { + assertEq(new Intl.Locale(tag).minimize().toString(), minimal); +}""" + ) + + println( + """ +if (typeof reportCompare === "function") + reportCompare(0, 0);""" + ) + + +def readCLDRVersionFromICU(): + icuDir = os.path.join(topsrcdir, "intl/icu/source") + if not os.path.isdir(icuDir): + raise RuntimeError("not a directory: {}".format(icuDir)) + + reVersion = re.compile(r'\s*cldrVersion\{"(\d+(?:\.\d+)?)"\}') + + for line in flines(os.path.join(icuDir, "data/misc/supplementalData.txt")): + m = reVersion.match(line) + if m: + version = m.group(1) + break + + if version is None: + raise RuntimeError("can't resolve CLDR version") + + return version + + +def updateCLDRLangTags(args): + """Update the LanguageTagGenerated.cpp file.""" + version = args.version + url = args.url + out = args.out + filename = args.file + + # Determine current CLDR version from ICU. + if version is None: + version = readCLDRVersionFromICU() + + url = url.replace("<VERSION>", version) + + print("Arguments:") + print("\tCLDR version: %s" % version) + print("\tDownload url: %s" % url) + if filename is not None: + print("\tLocal CLDR common.zip file: %s" % filename) + print("\tOutput file: %s" % out) + print("") + + data = { + "version": version, + } + + def readFiles(cldr_file): + with ZipFile(cldr_file) as zip_file: + data.update(readSupplementalData(zip_file)) + data.update(readUnicodeExtensions(zip_file)) + + print("Processing CLDR data...") + if filename is not None: + print("Always make sure you have the newest CLDR common.zip!") + with open(filename, "rb") as cldr_file: + readFiles(cldr_file) + else: + print("Downloading CLDR common.zip...") + with closing(urlopen(url)) as cldr_file: + cldr_data = io.BytesIO(cldr_file.read()) + readFiles(cldr_data) + + print("Writing Intl data...") + with io.open(out, mode="w", encoding="utf-8", newline="") as f: + println = partial(print, file=f) + + writeCLDRLanguageTagData(println, data, url) + + print("Writing Intl test data...") + js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__)) + test_file = os.path.join( + js_src_builtin_intl_dir, + "../../tests/non262/Intl/Locale/likely-subtags-generated.js", + ) + with io.open(test_file, mode="w", encoding="utf-8", newline="") as f: + println = partial(print, file=f) + + println("// |reftest| skip-if(!this.hasOwnProperty('Intl'))") + writeCLDRLanguageTagLikelySubtagsTest(println, data, url) + + +def flines(filepath, encoding="utf-8"): + """Open filepath and iterate over its content.""" + with io.open(filepath, mode="r", encoding=encoding) as f: + for line in f: + yield line + + +@total_ordering +class Zone(object): + """Time zone with optional file name.""" + + def __init__(self, name, filename=""): + self.name = name + self.filename = filename + + def __eq__(self, other): + return hasattr(other, "name") and self.name == other.name + + def __lt__(self, other): + return self.name < other.name + + def __hash__(self): + return hash(self.name) + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + + +class TzDataDir(object): + """tzdata source from a directory.""" + + def __init__(self, obj): + self.name = partial(os.path.basename, obj) + self.resolve = partial(os.path.join, obj) + self.basename = os.path.basename + self.isfile = os.path.isfile + self.listdir = partial(os.listdir, obj) + self.readlines = flines + + +class TzDataFile(object): + """tzdata source from a file (tar or gzipped).""" + + def __init__(self, obj): + self.name = lambda: os.path.splitext( + os.path.splitext(os.path.basename(obj))[0] + )[0] + self.resolve = obj.getmember + self.basename = attrgetter("name") + self.isfile = tarfile.TarInfo.isfile + self.listdir = obj.getnames + self.readlines = partial(self._tarlines, obj) + + def _tarlines(self, tar, m): + with closing(tar.extractfile(m)) as f: + for line in f: + yield line.decode("utf-8") + + +def validateTimeZones(zones, links): + """Validate the zone and link entries.""" + linkZones = set(links.keys()) + intersect = linkZones.intersection(zones) + if intersect: + raise RuntimeError("Links also present in zones: %s" % intersect) + + zoneNames = {z.name for z in zones} + linkTargets = set(links.values()) + if not linkTargets.issubset(zoneNames): + raise RuntimeError( + "Link targets not found: %s" % linkTargets.difference(zoneNames) + ) + + +def partition(iterable, *predicates): + def innerPartition(pred, it): + it1, it2 = tee(it) + return (filter(pred, it1), filterfalse(pred, it2)) + + if len(predicates) == 0: + return iterable + (left, right) = innerPartition(predicates[0], iterable) + if len(predicates) == 1: + return (left, right) + return tuple([left] + list(partition(right, *predicates[1:]))) + + +def listIANAFiles(tzdataDir): + def isTzFile(d, m, f): + return m(f) and d.isfile(d.resolve(f)) + + return filter( + partial(isTzFile, tzdataDir, re.compile("^[a-z0-9]+$").match), + tzdataDir.listdir(), + ) + + +def readIANAFiles(tzdataDir, files): + """Read all IANA time zone files from the given iterable.""" + nameSyntax = "[\w/+\-]+" + pZone = re.compile(r"Zone\s+(?P<name>%s)\s+.*" % nameSyntax) + pLink = re.compile( + r"Link\s+(?P<target>%s)\s+(?P<name>%s)(?:\s+#.*)?" % (nameSyntax, nameSyntax) + ) + + def createZone(line, fname): + match = pZone.match(line) + name = match.group("name") + return Zone(name, fname) + + def createLink(line, fname): + match = pLink.match(line) + (name, target) = match.group("name", "target") + return (Zone(name, fname), target) + + zones = set() + links = dict() + for filename in files: + filepath = tzdataDir.resolve(filename) + for line in tzdataDir.readlines(filepath): + if line.startswith("Zone"): + zones.add(createZone(line, filename)) + if line.startswith("Link"): + (link, target) = createLink(line, filename) + links[link] = target + + return (zones, links) + + +def readIANATimeZones(tzdataDir, ignoreBackzone, ignoreFactory): + """Read the IANA time zone information from `tzdataDir`.""" + + backzoneFiles = {"backzone"} + (bkfiles, tzfiles) = partition(listIANAFiles(tzdataDir), backzoneFiles.__contains__) + + # Read zone and link infos. + (zones, links) = readIANAFiles(tzdataDir, tzfiles) + (backzones, backlinks) = readIANAFiles(tzdataDir, bkfiles) + + # Remove the placeholder time zone "Factory". + if ignoreFactory: + zones.remove(Zone("Factory")) + + # Merge with backzone data. + if not ignoreBackzone: + zones |= backzones + links = { + name: target for name, target in links.items() if name not in backzones + } + links.update(backlinks) + + validateTimeZones(zones, links) + + return (zones, links) + + +def readICUResourceFile(filename): + """Read an ICU resource file. + + Yields (<table-name>, <startOrEnd>, <value>) for each table. + """ + + numberValue = r"-?\d+" + stringValue = r'".+?"' + + def asVector(val): + return r"%s(?:\s*,\s*%s)*" % (val, val) + + numberVector = asVector(numberValue) + stringVector = asVector(stringValue) + + reNumberVector = re.compile(numberVector) + reStringVector = re.compile(stringVector) + reNumberValue = re.compile(numberValue) + reStringValue = re.compile(stringValue) + + def parseValue(value): + m = reNumberVector.match(value) + if m: + return [int(v) for v in reNumberValue.findall(value)] + m = reStringVector.match(value) + if m: + return [v[1:-1] for v in reStringValue.findall(value)] + raise RuntimeError("unknown value type: %s" % value) + + def extractValue(values): + if len(values) == 0: + return None + if len(values) == 1: + return values[0] + return values + + def line(*args): + maybeMultiComments = r"(?:/\*[^*]*\*/)*" + maybeSingleComment = r"(?://.*)?" + lineStart = "^%s" % maybeMultiComments + lineEnd = "%s\s*%s$" % (maybeMultiComments, maybeSingleComment) + return re.compile(r"\s*".join(chain([lineStart], args, [lineEnd]))) + + tableName = r'(?P<quote>"?)(?P<name>.+?)(?P=quote)' + tableValue = r"(?P<value>%s|%s)" % (numberVector, stringVector) + + reStartTable = line(tableName, r"\{") + reEndTable = line(r"\}") + reSingleValue = line(r",?", tableValue, r",?") + reCompactTable = line(tableName, r"\{", tableValue, r"\}") + reEmptyLine = line() + + tables = [] + + def currentTable(): + return "|".join(tables) + + values = [] + for line in flines(filename, "utf-8-sig"): + line = line.strip() + if line == "": + continue + + m = reEmptyLine.match(line) + if m: + continue + + m = reStartTable.match(line) + if m: + assert len(values) == 0 + tables.append(m.group("name")) + continue + + m = reEndTable.match(line) + if m: + yield (currentTable(), extractValue(values)) + tables.pop() + values = [] + continue + + m = reCompactTable.match(line) + if m: + assert len(values) == 0 + tables.append(m.group("name")) + yield (currentTable(), extractValue(parseValue(m.group("value")))) + tables.pop() + continue + + m = reSingleValue.match(line) + if m and tables: + values.extend(parseValue(m.group("value"))) + continue + + raise RuntimeError("unknown entry: %s" % line) + + +def readICUTimeZonesFromTimezoneTypes(icuTzDir): + """Read the ICU time zone information from `icuTzDir`/timezoneTypes.txt + and returns the tuple (zones, links). + """ + typeMapTimeZoneKey = "timezoneTypes:table(nofallback)|typeMap|timezone|" + typeAliasTimeZoneKey = "timezoneTypes:table(nofallback)|typeAlias|timezone|" + + def toTimeZone(name): + return Zone(name.replace(":", "/")) + + zones = set() + links = dict() + + for name, value in readICUResourceFile(os.path.join(icuTzDir, "timezoneTypes.txt")): + if name.startswith(typeMapTimeZoneKey): + zones.add(toTimeZone(name[len(typeMapTimeZoneKey) :])) + if name.startswith(typeAliasTimeZoneKey): + links[toTimeZone(name[len(typeAliasTimeZoneKey) :])] = value + + validateTimeZones(zones, links) + + return (zones, links) + + +def readICUTimeZonesFromZoneInfo(icuTzDir): + """Read the ICU time zone information from `icuTzDir`/zoneinfo64.txt + and returns the tuple (zones, links). + """ + zoneKey = "zoneinfo64:table(nofallback)|Zones:array|:table" + linkKey = "zoneinfo64:table(nofallback)|Zones:array|:int" + namesKey = "zoneinfo64:table(nofallback)|Names" + + tzId = 0 + tzLinks = dict() + tzNames = [] + + for name, value in readICUResourceFile(os.path.join(icuTzDir, "zoneinfo64.txt")): + if name == zoneKey: + tzId += 1 + elif name == linkKey: + tzLinks[tzId] = int(value) + tzId += 1 + elif name == namesKey: + tzNames.extend(value) + + links = {Zone(tzNames[zone]): tzNames[target] for (zone, target) in tzLinks.items()} + zones = {Zone(v) for v in tzNames if Zone(v) not in links} + + validateTimeZones(zones, links) + + return (zones, links) + + +def readICUTimeZones(icuDir, icuTzDir, ignoreFactory): + # zoneinfo64.txt contains the supported time zones by ICU. This data is + # generated from tzdata files, it doesn't include "backzone" in stock ICU. + (zoneinfoZones, zoneinfoLinks) = readICUTimeZonesFromZoneInfo(icuTzDir) + + # timezoneTypes.txt contains the canonicalization information for ICU. This + # data is generated from CLDR files. It includes data about time zones from + # tzdata's "backzone" file. + (typesZones, typesLinks) = readICUTimeZonesFromTimezoneTypes(icuTzDir) + + # Remove the placeholder time zone "Factory". + # See also <https://github.com/eggert/tz/blob/master/factory>. + if ignoreFactory: + zoneinfoZones.remove(Zone("Factory")) + + # Remove the ICU placeholder time zone "Etc/Unknown". + # See also <https://unicode.org/reports/tr35/#Time_Zone_Identifiers>. + for zones in (zoneinfoZones, typesZones): + zones.remove(Zone("Etc/Unknown")) + + # Remove any outdated ICU links. + for links in (zoneinfoLinks, typesLinks): + for zone in otherICULegacyLinks().keys(): + if zone not in links: + raise KeyError(f"Can't remove non-existent link from '{zone}'") + del links[zone] + + # Information in zoneinfo64 should be a superset of timezoneTypes. + def inZoneInfo64(zone): + return zone in zoneinfoZones or zone in zoneinfoLinks + + notFoundInZoneInfo64 = [zone for zone in typesZones if not inZoneInfo64(zone)] + if notFoundInZoneInfo64: + raise RuntimeError( + "Missing time zones in zoneinfo64.txt: %s" % notFoundInZoneInfo64 + ) + + notFoundInZoneInfo64 = [ + zone for zone in typesLinks.keys() if not inZoneInfo64(zone) + ] + if notFoundInZoneInfo64: + raise RuntimeError( + "Missing time zones in zoneinfo64.txt: %s" % notFoundInZoneInfo64 + ) + + # zoneinfo64.txt only defines the supported time zones by ICU, the canonicalization + # rules are defined through timezoneTypes.txt. Merge both to get the actual zones + # and links used by ICU. + icuZones = set( + chain( + (zone for zone in zoneinfoZones if zone not in typesLinks), + (zone for zone in typesZones), + ) + ) + icuLinks = dict( + chain( + ( + (zone, target) + for (zone, target) in zoneinfoLinks.items() + if zone not in typesZones + ), + ((zone, target) for (zone, target) in typesLinks.items()), + ) + ) + + return (icuZones, icuLinks) + + +def readICULegacyZones(icuDir): + """Read the ICU legacy time zones from `icuTzDir`/tools/tzcode/icuzones + and returns the tuple (zones, links). + """ + tzdir = TzDataDir(os.path.join(icuDir, "tools/tzcode")) + + # Per spec we must recognize only IANA time zones and links, but ICU + # recognizes various legacy, non-IANA time zones and links. Compute these + # non-IANA time zones and links. + + # Most legacy, non-IANA time zones and links are in the icuzones file. + (zones, links) = readIANAFiles(tzdir, ["icuzones"]) + + # Remove the ICU placeholder time zone "Etc/Unknown". + # See also <https://unicode.org/reports/tr35/#Time_Zone_Identifiers>. + zones.remove(Zone("Etc/Unknown")) + + # A handful of non-IANA zones/links are not in icuzones and must be added + # manually so that we won't invoke ICU with them. + for (zone, target) in otherICULegacyLinks().items(): + if zone in links: + if links[zone] != target: + raise KeyError( + f"Can't overwrite link '{zone} -> {links[zone]}' with '{target}'" + ) + else: + print( + f"Info: Link '{zone} -> {target}' can be removed from otherICULegacyLinks()" + ) + links[zone] = target + + return (zones, links) + + +def otherICULegacyLinks(): + """The file `icuTzDir`/tools/tzcode/icuzones contains all ICU legacy time + zones with the exception of time zones which are removed by IANA after an + ICU release. + + For example ICU 67 uses tzdata2018i, but tzdata2020b removed the link from + "US/Pacific-New" to "America/Los_Angeles". ICU standalone tzdata updates + don't include modified icuzones files, so we must manually record any IANA + modifications here. + + After an ICU update, we can remove any no longer needed entries from this + function by checking if the relevant entries are now included in icuzones. + """ + + return { + # Current ICU is up-to-date with IANA, so this dict is empty. + } + + +def icuTzDataVersion(icuTzDir): + """Read the ICU time zone version from `icuTzDir`/zoneinfo64.txt.""" + + def searchInFile(pattern, f): + p = re.compile(pattern) + for line in flines(f, "utf-8-sig"): + m = p.search(line) + if m: + return m.group(1) + return None + + zoneinfo = os.path.join(icuTzDir, "zoneinfo64.txt") + if not os.path.isfile(zoneinfo): + raise RuntimeError("file not found: %s" % zoneinfo) + version = searchInFile("^//\s+tz version:\s+([0-9]{4}[a-z])$", zoneinfo) + if version is None: + raise RuntimeError( + "%s does not contain a valid tzdata version string" % zoneinfo + ) + return version + + +def findIncorrectICUZones(ianaZones, ianaLinks, icuZones, icuLinks, ignoreBackzone): + """Find incorrect ICU zone entries.""" + + def isIANATimeZone(zone): + return zone in ianaZones or zone in ianaLinks + + def isICUTimeZone(zone): + return zone in icuZones or zone in icuLinks + + def isICULink(zone): + return zone in icuLinks + + # All IANA zones should be present in ICU. + missingTimeZones = [zone for zone in ianaZones if not isICUTimeZone(zone)] + # Normally zones in backzone are also present as links in one of the other + # time zone files. The only exception to this rule is the Asia/Hanoi time + # zone, this zone is only present in the backzone file. + expectedMissing = [] if ignoreBackzone else [Zone("Asia/Hanoi")] + if missingTimeZones != expectedMissing: + raise RuntimeError( + "Not all zones are present in ICU, did you forget " + "to run intl/update-tzdata.sh? %s" % missingTimeZones + ) + + # Zones which are only present in ICU? + additionalTimeZones = [zone for zone in icuZones if not isIANATimeZone(zone)] + if additionalTimeZones: + raise RuntimeError( + "Additional zones present in ICU, did you forget " + "to run intl/update-tzdata.sh? %s" % additionalTimeZones + ) + + # Zones which are marked as links in ICU. + result = ((zone, icuLinks[zone]) for zone in ianaZones if isICULink(zone)) + + # Remove unnecessary UTC mappings. + utcnames = ["Etc/UTC", "Etc/UCT", "Etc/GMT"] + result = ((zone, target) for (zone, target) in result if zone.name not in utcnames) + + return sorted(result, key=itemgetter(0)) + + +def findIncorrectICULinks(ianaZones, ianaLinks, icuZones, icuLinks): + """Find incorrect ICU link entries.""" + + def isIANATimeZone(zone): + return zone in ianaZones or zone in ianaLinks + + def isICUTimeZone(zone): + return zone in icuZones or zone in icuLinks + + def isICULink(zone): + return zone in icuLinks + + def isICUZone(zone): + return zone in icuZones + + # All links should be present in ICU. + missingTimeZones = [zone for zone in ianaLinks.keys() if not isICUTimeZone(zone)] + if missingTimeZones: + raise RuntimeError( + "Not all zones are present in ICU, did you forget " + "to run intl/update-tzdata.sh? %s" % missingTimeZones + ) + + # Links which are only present in ICU? + additionalTimeZones = [zone for zone in icuLinks.keys() if not isIANATimeZone(zone)] + if additionalTimeZones: + raise RuntimeError( + "Additional links present in ICU, did you forget " + "to run intl/update-tzdata.sh? %s" % additionalTimeZones + ) + + result = chain( + # IANA links which have a different target in ICU. + ( + (zone, target, icuLinks[zone]) + for (zone, target) in ianaLinks.items() + if isICULink(zone) and target != icuLinks[zone] + ), + # IANA links which are zones in ICU. + ( + (zone, target, zone.name) + for (zone, target) in ianaLinks.items() + if isICUZone(zone) + ), + ) + + # Remove unnecessary UTC mappings. + utcnames = ["Etc/UTC", "Etc/UCT", "Etc/GMT"] + result = ( + (zone, target, icuTarget) + for (zone, target, icuTarget) in result + if target not in utcnames or icuTarget not in utcnames + ) + + return sorted(result, key=itemgetter(0)) + + +generatedFileWarning = "// Generated by make_intl_data.py. DO NOT EDIT." +tzdataVersionComment = "// tzdata version = {0}" + + +def processTimeZones( + tzdataDir, icuDir, icuTzDir, version, ignoreBackzone, ignoreFactory, out +): + """Read the time zone info and create a new time zone cpp file.""" + print("Processing tzdata mapping...") + (ianaZones, ianaLinks) = readIANATimeZones(tzdataDir, ignoreBackzone, ignoreFactory) + (icuZones, icuLinks) = readICUTimeZones(icuDir, icuTzDir, ignoreFactory) + (legacyZones, legacyLinks) = readICULegacyZones(icuDir) + + # Remove all legacy ICU time zones. + icuZones = {zone for zone in icuZones if zone not in legacyZones} + icuLinks = { + zone: target for (zone, target) in icuLinks.items() if zone not in legacyLinks + } + + incorrectZones = findIncorrectICUZones( + ianaZones, ianaLinks, icuZones, icuLinks, ignoreBackzone + ) + if not incorrectZones: + print("<<< No incorrect ICU time zones found, please update Intl.js! >>>") + print("<<< Maybe https://ssl.icu-project.org/trac/ticket/12044 was fixed? >>>") + + incorrectLinks = findIncorrectICULinks(ianaZones, ianaLinks, icuZones, icuLinks) + if not incorrectLinks: + print("<<< No incorrect ICU time zone links found, please update Intl.js! >>>") + print("<<< Maybe https://ssl.icu-project.org/trac/ticket/12044 was fixed? >>>") + + print("Writing Intl tzdata file...") + with io.open(out, mode="w", encoding="utf-8", newline="") as f: + println = partial(print, file=f) + + println(generatedFileWarning) + println(tzdataVersionComment.format(version)) + println("") + + println("#ifndef builtin_intl_TimeZoneDataGenerated_h") + println("#define builtin_intl_TimeZoneDataGenerated_h") + println("") + + println("namespace js {") + println("namespace timezone {") + println("") + + println("// Format:") + println('// "ZoneName" // ICU-Name [time zone file]') + println("const char* const ianaZonesTreatedAsLinksByICU[] = {") + for (zone, icuZone) in incorrectZones: + println(' "%s", // %s [%s]' % (zone, icuZone, zone.filename)) + println("};") + println("") + + println("// Format:") + println('// "LinkName", "Target" // ICU-Target [time zone file]') + println("struct LinkAndTarget") + println("{") + println(" const char* const link;") + println(" const char* const target;") + println("};") + println("") + println("const LinkAndTarget ianaLinksCanonicalizedDifferentlyByICU[] = {") + for (zone, target, icuTarget) in incorrectLinks: + println( + ' { "%s", "%s" }, // %s [%s]' + % (zone, target, icuTarget, zone.filename) + ) + println("};") + println("") + + println( + "// Legacy ICU time zones, these are not valid IANA time zone names. We also" + ) + println("// disallow the old and deprecated System V time zones.") + println( + "// https://ssl.icu-project.org/repos/icu/trunk/icu4c/source/tools/tzcode/icuzones" + ) # NOQA: E501 + println("const char* const legacyICUTimeZones[] = {") + for zone in chain(sorted(legacyLinks.keys()), sorted(legacyZones)): + println(' "%s",' % zone) + println("};") + println("") + + println("} // namespace timezone") + println("} // namespace js") + println("") + println("#endif /* builtin_intl_TimeZoneDataGenerated_h */") + + +def updateBackzoneLinks(tzdataDir, links): + def withZone(fn): + return lambda zone_target: fn(zone_target[0]) + + (backzoneZones, backzoneLinks) = readIANAFiles(tzdataDir, ["backzone"]) + (stableZones, updatedLinks, updatedZones) = partition( + links.items(), + # Link not changed in backzone. + withZone(lambda zone: zone not in backzoneLinks and zone not in backzoneZones), + # Link has a new target. + withZone(lambda zone: zone in backzoneLinks), + ) + # Keep stable zones and links with updated target. + return dict( + chain( + stableZones, + map(withZone(lambda zone: (zone, backzoneLinks[zone])), updatedLinks), + ) + ) + + +def generateTzDataLinkTestContent(testDir, version, fileName, description, links): + with io.open( + os.path.join(testDir, fileName), mode="w", encoding="utf-8", newline="" + ) as f: + println = partial(print, file=f) + + println('// |reftest| skip-if(!this.hasOwnProperty("Intl"))') + println("") + println(generatedFileWarning) + println(tzdataVersionComment.format(version)) + println( + """ +const tzMapper = [ + x => x, + x => x.toUpperCase(), + x => x.toLowerCase(), +]; +""" + ) + + println(description) + println("const links = {") + for (zone, target) in sorted(links, key=itemgetter(0)): + println(' "%s": "%s",' % (zone, target)) + println("};") + + println( + """ +for (let [linkName, target] of Object.entries(links)) { + if (target === "Etc/UTC" || target === "Etc/GMT") + target = "UTC"; + + for (let map of tzMapper) { + let dtf = new Intl.DateTimeFormat(undefined, {timeZone: map(linkName)}); + let resolvedTimeZone = dtf.resolvedOptions().timeZone; + assertEq(resolvedTimeZone, target, `${linkName} -> ${target}`); + } +} +""" + ) + println( + """ +if (typeof reportCompare === "function") + reportCompare(0, 0, "ok"); +""" + ) + + +def generateTzDataTestBackwardLinks(tzdataDir, version, ignoreBackzone, testDir): + (zones, links) = readIANAFiles(tzdataDir, ["backward"]) + assert len(zones) == 0 + + if not ignoreBackzone: + links = updateBackzoneLinks(tzdataDir, links) + + generateTzDataLinkTestContent( + testDir, + version, + "timeZone_backward_links.js", + "// Link names derived from IANA Time Zone Database, backward file.", + links.items(), + ) + + +def generateTzDataTestNotBackwardLinks(tzdataDir, version, ignoreBackzone, testDir): + tzfiles = filterfalse( + {"backward", "backzone"}.__contains__, listIANAFiles(tzdataDir) + ) + (zones, links) = readIANAFiles(tzdataDir, tzfiles) + + if not ignoreBackzone: + links = updateBackzoneLinks(tzdataDir, links) + + generateTzDataLinkTestContent( + testDir, + version, + "timeZone_notbackward_links.js", + "// Link names derived from IANA Time Zone Database, excluding backward file.", + links.items(), + ) + + +def generateTzDataTestBackzone(tzdataDir, version, ignoreBackzone, testDir): + backzoneFiles = {"backzone"} + (bkfiles, tzfiles) = partition(listIANAFiles(tzdataDir), backzoneFiles.__contains__) + + # Read zone and link infos. + (zones, links) = readIANAFiles(tzdataDir, tzfiles) + (backzones, backlinks) = readIANAFiles(tzdataDir, bkfiles) + + if not ignoreBackzone: + comment = """\ +// This file was generated with historical, pre-1970 backzone information +// respected. Therefore, every zone key listed below is its own Zone, not +// a Link to a modern-day target as IANA ignoring backzones would say. + +""" + else: + comment = """\ +// This file was generated while ignoring historical, pre-1970 backzone +// information. Therefore, every zone key listed below is part of a Link +// whose target is the corresponding value. + +""" + + generateTzDataLinkTestContent( + testDir, + version, + "timeZone_backzone.js", + comment + "// Backzone zones derived from IANA Time Zone Database.", + ( + (zone, zone if not ignoreBackzone else links[zone]) + for zone in backzones + if zone in links + ), + ) + + +def generateTzDataTestBackzoneLinks(tzdataDir, version, ignoreBackzone, testDir): + backzoneFiles = {"backzone"} + (bkfiles, tzfiles) = partition(listIANAFiles(tzdataDir), backzoneFiles.__contains__) + + # Read zone and link infos. + (zones, links) = readIANAFiles(tzdataDir, tzfiles) + (backzones, backlinks) = readIANAFiles(tzdataDir, bkfiles) + + if not ignoreBackzone: + comment = """\ +// This file was generated with historical, pre-1970 backzone information +// respected. Therefore, every zone key listed below points to a target +// in the backzone file and not to its modern-day target as IANA ignoring +// backzones would say. + +""" + else: + comment = """\ +// This file was generated while ignoring historical, pre-1970 backzone +// information. Therefore, every zone key listed below is part of a Link +// whose target is the corresponding value ignoring any backzone entries. + +""" + + generateTzDataLinkTestContent( + testDir, + version, + "timeZone_backzone_links.js", + comment + "// Backzone links derived from IANA Time Zone Database.", + ( + (zone, target if not ignoreBackzone else links[zone]) + for (zone, target) in backlinks.items() + ), + ) + + +def generateTzDataTestVersion(tzdataDir, version, testDir): + fileName = "timeZone_version.js" + + with io.open( + os.path.join(testDir, fileName), mode="w", encoding="utf-8", newline="" + ) as f: + println = partial(print, file=f) + + println('// |reftest| skip-if(!this.hasOwnProperty("Intl"))') + println("") + println(generatedFileWarning) + println(tzdataVersionComment.format(version)) + println("""const tzdata = "{0}";""".format(version)) + + println( + """ +if (typeof getICUOptions === "undefined") { + var getICUOptions = SpecialPowers.Cu.getJSTestingFunctions().getICUOptions; +} + +var options = getICUOptions(); + +assertEq(options.tzdata, tzdata); + +if (typeof reportCompare === "function") + reportCompare(0, 0, "ok"); +""" + ) + + +def generateTzDataTestCanonicalZones( + tzdataDir, version, ignoreBackzone, ignoreFactory, testDir +): + fileName = "supportedValuesOf-timeZones-canonical.js" + + # Read zone and link infos. + (ianaZones, _) = readIANATimeZones(tzdataDir, ignoreBackzone, ignoreFactory) + + # Replace Etc/GMT and Etc/UTC with UTC. + ianaZones.remove(Zone("Etc/GMT")) + ianaZones.remove(Zone("Etc/UTC")) + ianaZones.add(Zone("UTC")) + + # See findIncorrectICUZones() for why Asia/Hanoi has to be special-cased. + ianaZones.remove(Zone("Asia/Hanoi")) + + if not ignoreBackzone: + comment = """\ +// This file was generated with historical, pre-1970 backzone information +// respected. +""" + else: + comment = """\ +// This file was generated while ignoring historical, pre-1970 backzone +// information. +""" + + with io.open( + os.path.join(testDir, fileName), mode="w", encoding="utf-8", newline="" + ) as f: + println = partial(print, file=f) + + println('// |reftest| skip-if(!this.hasOwnProperty("Intl"))') + println("") + println(generatedFileWarning) + println(tzdataVersionComment.format(version)) + println("") + println(comment) + + println("const zones = [") + for zone in sorted(ianaZones): + println(f' "{zone}",') + println("];") + + println( + """ +let supported = Intl.supportedValuesOf("timeZone"); + +assertEqArray(supported, zones); + +if (typeof reportCompare === "function") + reportCompare(0, 0, "ok"); +""" + ) + + +def generateTzDataTests(tzdataDir, version, ignoreBackzone, ignoreFactory, testDir): + dtfTestDir = os.path.join(testDir, "DateTimeFormat") + if not os.path.isdir(dtfTestDir): + raise RuntimeError("not a directory: %s" % dtfTestDir) + + generateTzDataTestBackwardLinks(tzdataDir, version, ignoreBackzone, dtfTestDir) + generateTzDataTestNotBackwardLinks(tzdataDir, version, ignoreBackzone, dtfTestDir) + generateTzDataTestBackzone(tzdataDir, version, ignoreBackzone, dtfTestDir) + generateTzDataTestBackzoneLinks(tzdataDir, version, ignoreBackzone, dtfTestDir) + generateTzDataTestVersion(tzdataDir, version, dtfTestDir) + generateTzDataTestCanonicalZones( + tzdataDir, version, ignoreBackzone, ignoreFactory, testDir + ) + + +def updateTzdata(topsrcdir, args): + """Update the time zone cpp file.""" + + icuDir = os.path.join(topsrcdir, "intl/icu/source") + if not os.path.isdir(icuDir): + raise RuntimeError("not a directory: %s" % icuDir) + + icuTzDir = os.path.join(topsrcdir, "intl/tzdata/source") + if not os.path.isdir(icuTzDir): + raise RuntimeError("not a directory: %s" % icuTzDir) + + intlTestDir = os.path.join(topsrcdir, "js/src/tests/non262/Intl") + if not os.path.isdir(intlTestDir): + raise RuntimeError("not a directory: %s" % intlTestDir) + + tzDir = args.tz + if tzDir is not None and not (os.path.isdir(tzDir) or os.path.isfile(tzDir)): + raise RuntimeError("not a directory or file: %s" % tzDir) + ignoreBackzone = args.ignore_backzone + # TODO: Accept or ignore the placeholder time zone "Factory"? + ignoreFactory = False + out = args.out + + version = icuTzDataVersion(icuTzDir) + url = ( + "https://www.iana.org/time-zones/repository/releases/tzdata%s.tar.gz" % version + ) + + print("Arguments:") + print("\ttzdata version: %s" % version) + print("\ttzdata URL: %s" % url) + print("\ttzdata directory|file: %s" % tzDir) + print("\tICU directory: %s" % icuDir) + print("\tICU timezone directory: %s" % icuTzDir) + print("\tIgnore backzone file: %s" % ignoreBackzone) + print("\tOutput file: %s" % out) + print("") + + def updateFrom(f): + if os.path.isfile(f) and tarfile.is_tarfile(f): + with tarfile.open(f, "r:*") as tar: + processTimeZones( + TzDataFile(tar), + icuDir, + icuTzDir, + version, + ignoreBackzone, + ignoreFactory, + out, + ) + generateTzDataTests( + TzDataFile(tar), version, ignoreBackzone, ignoreFactory, intlTestDir + ) + elif os.path.isdir(f): + processTimeZones( + TzDataDir(f), + icuDir, + icuTzDir, + version, + ignoreBackzone, + ignoreFactory, + out, + ) + generateTzDataTests( + TzDataDir(f), version, ignoreBackzone, ignoreFactory, intlTestDir + ) + else: + raise RuntimeError("unknown format") + + if tzDir is None: + print("Downloading tzdata file...") + with closing(urlopen(url)) as tzfile: + fname = urlsplit(tzfile.geturl()).path.split("/")[-1] + with tempfile.NamedTemporaryFile(suffix=fname) as tztmpfile: + print("File stored in %s" % tztmpfile.name) + tztmpfile.write(tzfile.read()) + tztmpfile.flush() + updateFrom(tztmpfile.name) + else: + updateFrom(tzDir) + + +def readCurrencyFile(tree): + reCurrency = re.compile(r"^[A-Z]{3}$") + reIntMinorUnits = re.compile(r"^\d+$") + + for country in tree.iterfind(".//CcyNtry"): + # Skip entry if no currency information is available. + currency = country.findtext("Ccy") + if currency is None: + continue + assert reCurrency.match(currency) + + minorUnits = country.findtext("CcyMnrUnts") + assert minorUnits is not None + + # Skip all entries without minorUnits or which use the default minorUnits. + if reIntMinorUnits.match(minorUnits) and int(minorUnits) != 2: + currencyName = country.findtext("CcyNm") + countryName = country.findtext("CtryNm") + yield (currency, int(minorUnits), currencyName, countryName) + + +def writeCurrencyFile(published, currencies, out): + with io.open(out, mode="w", encoding="utf-8", newline="") as f: + println = partial(print, file=f) + + println(generatedFileWarning) + println("// Version: {}".format(published)) + + println( + """ +/** + * Mapping from currency codes to the number of decimal digits used for them. + * Default is 2 digits. + * + * Spec: ISO 4217 Currency and Funds Code List. + * http://www.currency-iso.org/en/home/tables/table-a1.html + */""" + ) + println("var currencyDigits = {") + for (currency, entries) in groupby( + sorted(currencies, key=itemgetter(0)), itemgetter(0) + ): + for (_, minorUnits, currencyName, countryName) in entries: + println(" // {} ({})".format(currencyName, countryName)) + println(" {}: {},".format(currency, minorUnits)) + println("};") + + +def updateCurrency(topsrcdir, args): + """Update the CurrencyDataGenerated.js file.""" + import xml.etree.ElementTree as ET + from random import randint + + url = args.url + out = args.out + filename = args.file + + print("Arguments:") + print("\tDownload url: %s" % url) + print("\tLocal currency file: %s" % filename) + print("\tOutput file: %s" % out) + print("") + + def updateFrom(currencyFile): + print("Processing currency code list file...") + tree = ET.parse(currencyFile) + published = tree.getroot().attrib["Pblshd"] + currencies = readCurrencyFile(tree) + + print("Writing CurrencyData file...") + writeCurrencyFile(published, currencies, out) + + if filename is not None: + print("Always make sure you have the newest currency code list file!") + updateFrom(filename) + else: + print("Downloading currency & funds code list...") + request = UrlRequest(url) + request.add_header( + "User-agent", + "Mozilla/5.0 (Mobile; rv:{0}.0) Gecko/{0}.0 Firefox/{0}.0".format( + randint(1, 999) + ), + ) + with closing(urlopen(request)) as currencyFile: + fname = urlsplit(currencyFile.geturl()).path.split("/")[-1] + with tempfile.NamedTemporaryFile(suffix=fname) as currencyTmpFile: + print("File stored in %s" % currencyTmpFile.name) + currencyTmpFile.write(currencyFile.read()) + currencyTmpFile.flush() + updateFrom(currencyTmpFile.name) + + +def writeUnicodeExtensionsMappings(println, mapping, extension): + println( + """ +template <size_t Length> +static inline bool Is{0}Key(mozilla::Span<const char> key, const char (&str)[Length]) {{ + static_assert(Length == {0}KeyLength + 1, + "{0} extension key is two characters long"); + return memcmp(key.data(), str, Length - 1) == 0; +}} + +template <size_t Length> +static inline bool Is{0}Type(mozilla::Span<const char> type, const char (&str)[Length]) {{ + static_assert(Length > {0}KeyLength + 1, + "{0} extension type contains more than two characters"); + return type.size() == (Length - 1) && + memcmp(type.data(), str, Length - 1) == 0; +}} +""".format( + extension + ).rstrip( + "\n" + ) + ) + + linear_search_max_length = 4 + + needs_binary_search = any( + len(replacements.items()) > linear_search_max_length + for replacements in mapping.values() + ) + + if needs_binary_search: + println( + """ +static int32_t Compare{0}Type(const char* a, mozilla::Span<const char> b) {{ + MOZ_ASSERT(!std::char_traits<char>::find(b.data(), b.size(), '\\0'), + "unexpected null-character in string"); + + using UnsignedChar = unsigned char; + for (size_t i = 0; i < b.size(); i++) {{ + // |a| is zero-terminated and |b| doesn't contain a null-terminator. So if + // we've reached the end of |a|, the below if-statement will always be true. + // That ensures we don't read past the end of |a|. + if (int32_t r = UnsignedChar(a[i]) - UnsignedChar(b[i])) {{ + return r; + }} + }} + + // Return zero if both strings are equal or a positive number if |b| is a + // prefix of |a|. + return int32_t(UnsignedChar(a[b.size()])); +}} + +template <size_t Length> +static inline const char* Search{0}Replacement( + const char* (&types)[Length], const char* (&aliases)[Length], + mozilla::Span<const char> type) {{ + + auto p = std::lower_bound(std::begin(types), std::end(types), type, + [](const auto& a, const auto& b) {{ + return Compare{0}Type(a, b) < 0; + }}); + if (p != std::end(types) && Compare{0}Type(*p, type) == 0) {{ + return aliases[std::distance(std::begin(types), p)]; + }} + return nullptr; +}} +""".format( + extension + ).rstrip( + "\n" + ) + ) + + println( + """ +/** + * Mapping from deprecated BCP 47 {0} extension types to their preferred + * values. + * + * Spec: https://www.unicode.org/reports/tr35/#Unicode_Locale_Extension_Data_Files + * Spec: https://www.unicode.org/reports/tr35/#t_Extension + */ +const char* mozilla::intl::Locale::Replace{0}ExtensionType( + mozilla::Span<const char> key, mozilla::Span<const char> type) {{ + MOZ_ASSERT(key.size() == {0}KeyLength); + MOZ_ASSERT(IsCanonicallyCased{0}Key(key)); + + MOZ_ASSERT(type.size() > {0}KeyLength); + MOZ_ASSERT(IsCanonicallyCased{0}Type(type)); +""".format( + extension + ) + ) + + def to_hash_key(replacements): + return str(sorted(replacements.items())) + + def write_array(subtags, name, length): + max_entries = (80 - len(" ")) // (length + len('"", ')) + + println(" static const char* {}[{}] = {{".format(name, len(subtags))) + + for entries in grouper(subtags, max_entries): + entries = ( + '"{}"'.format(tag).center(length + 2) + for tag in entries + if tag is not None + ) + println(" {},".format(", ".join(entries))) + + println(" };") + + # Merge duplicate keys. + key_aliases = {} + for (key, replacements) in sorted(mapping.items(), key=itemgetter(0)): + hash_key = to_hash_key(replacements) + if hash_key not in key_aliases: + key_aliases[hash_key] = [] + else: + key_aliases[hash_key].append(key) + + first_key = True + for (key, replacements) in sorted(mapping.items(), key=itemgetter(0)): + hash_key = to_hash_key(replacements) + if key in key_aliases[hash_key]: + continue + + cond = ( + 'Is{}Key(key, "{}")'.format(extension, k) + for k in [key] + key_aliases[hash_key] + ) + + if_kind = "if" if first_key else "else if" + cond = (" ||\n" + " " * (2 + len(if_kind) + 2)).join(cond) + println( + """ + {} ({}) {{""".format( + if_kind, cond + ).strip( + "\n" + ) + ) + first_key = False + + replacements = sorted(replacements.items(), key=itemgetter(0)) + + if len(replacements) > linear_search_max_length: + types = [t for (t, _) in replacements] + preferred = [r for (_, r) in replacements] + max_len = max(len(k) for k in types + preferred) + + write_array(types, "types", max_len) + write_array(preferred, "aliases", max_len) + println( + """ + return Search{}Replacement(types, aliases, type); +""".format( + extension + ).strip( + "\n" + ) + ) + else: + for (type, replacement) in replacements: + println( + """ + if (Is{}Type(type, "{}")) {{ + return "{}"; + }}""".format( + extension, type, replacement + ).strip( + "\n" + ) + ) + + println( + """ + }""".lstrip( + "\n" + ) + ) + + println( + """ + return nullptr; +} +""".strip( + "\n" + ) + ) + + +def readICUUnitResourceFile(filepath): + """Return a set of unit descriptor pairs where the first entry denotes the unit type and the + second entry the unit name. + + Example: + + root{ + units{ + compound{ + } + coordinate{ + } + length{ + meter{ + } + } + } + unitsNarrow:alias{"/LOCALE/unitsShort"} + unitsShort{ + duration{ + day{ + } + day-person:alias{"/LOCALE/unitsShort/duration/day"} + } + length{ + meter{ + } + } + } + } + + Returns {("length", "meter"), ("duration", "day"), ("duration", "day-person")} + """ + + start_table_re = re.compile(r"^([\w\-%:\"]+)\{$") + end_table_re = re.compile(r"^\}$") + table_entry_re = re.compile(r"^([\w\-%:\"]+)\{\"(.*?)\"\}$") + + # The current resource table. + table = {} + + # List of parent tables when parsing. + parents = [] + + # Track multi-line comments state. + in_multiline_comment = False + + for line in flines(filepath, "utf-8-sig"): + # Remove leading and trailing whitespace. + line = line.strip() + + # Skip over comments. + if in_multiline_comment: + if line.endswith("*/"): + in_multiline_comment = False + continue + + if line.startswith("//"): + continue + + if line.startswith("/*"): + in_multiline_comment = True + continue + + # Try to match the start of a table, e.g. `length{` or `meter{`. + match = start_table_re.match(line) + if match: + parents.append(table) + table_name = match.group(1) + new_table = {} + table[table_name] = new_table + table = new_table + continue + + # Try to match the end of a table. + match = end_table_re.match(line) + if match: + table = parents.pop() + continue + + # Try to match a table entry, e.g. `dnam{"meter"}`. + match = table_entry_re.match(line) + if match: + entry_key = match.group(1) + entry_value = match.group(2) + table[entry_key] = entry_value + continue + + raise Exception("unexpected line: '{}' in {}".format(line, filepath)) + + assert len(parents) == 0, "Not all tables closed" + assert len(table) == 1, "More than one root table" + + # Remove the top-level language identifier table. + (_, unit_table) = table.popitem() + + # Add all units for the three display formats "units", "unitsNarrow", and "unitsShort". + # But exclude the pseudo-units "compound" and "ccoordinate". + return { + (unit_type, unit_name if not unit_name.endswith(":alias") else unit_name[:-6]) + for unit_display in ("units", "unitsNarrow", "unitsShort") + if unit_display in unit_table + for (unit_type, unit_names) in unit_table[unit_display].items() + if unit_type != "compound" and unit_type != "coordinate" + for unit_name in unit_names.keys() + } + + +def computeSupportedUnits(all_units, sanctioned_units): + """Given the set of all possible ICU unit identifiers and the set of sanctioned unit + identifiers, compute the set of effectively supported ICU unit identifiers. + """ + + def find_match(unit): + unit_match = [ + (unit_type, unit_name) + for (unit_type, unit_name) in all_units + if unit_name == unit + ] + if unit_match: + assert len(unit_match) == 1 + return unit_match[0] + return None + + def compound_unit_identifiers(): + for numerator in sanctioned_units: + for denominator in sanctioned_units: + yield "{}-per-{}".format(numerator, denominator) + + supported_simple_units = {find_match(unit) for unit in sanctioned_units} + assert None not in supported_simple_units + + supported_compound_units = { + unit_match + for unit_match in (find_match(unit) for unit in compound_unit_identifiers()) + if unit_match + } + + return supported_simple_units | supported_compound_units + + +def readICUDataFilterForUnits(data_filter_file): + with io.open(data_filter_file, mode="r", encoding="utf-8") as f: + data_filter = json.load(f) + + # Find the rule set for the "unit_tree". + unit_tree_rules = [ + entry["rules"] + for entry in data_filter["resourceFilters"] + if entry["categories"] == ["unit_tree"] + ] + assert len(unit_tree_rules) == 1 + + # Compute the list of included units from that rule set. The regular expression must match + # "+/*/length/meter" and mustn't match either "-/*" or "+/*/compound". + included_unit_re = re.compile(r"^\+/\*/(.+?)/(.+)$") + filtered_units = (included_unit_re.match(unit) for unit in unit_tree_rules[0]) + + return {(unit.group(1), unit.group(2)) for unit in filtered_units if unit} + + +def writeSanctionedSimpleUnitIdentifiersFiles(all_units, sanctioned_units): + js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__)) + intl_components_src_dir = os.path.join( + js_src_builtin_intl_dir, "../../../../intl/components/src" + ) + + def find_unit_type(unit): + result = [ + unit_type for (unit_type, unit_name) in all_units if unit_name == unit + ] + assert result and len(result) == 1 + return result[0] + + sanctioned_js_file = os.path.join( + js_src_builtin_intl_dir, "SanctionedSimpleUnitIdentifiersGenerated.js" + ) + with io.open(sanctioned_js_file, mode="w", encoding="utf-8", newline="") as f: + println = partial(print, file=f) + + sanctioned_units_object = json.dumps( + {unit: True for unit in sorted(sanctioned_units)}, + sort_keys=True, + indent=2, + separators=(",", ": "), + ) + + println(generatedFileWarning) + + println( + """ +/** + * The list of currently supported simple unit identifiers. + * + * Intl.NumberFormat Unified API Proposal + */""" + ) + + println("// prettier-ignore") + println( + "var sanctionedSimpleUnitIdentifiers = {};".format(sanctioned_units_object) + ) + + sanctioned_h_file = os.path.join(intl_components_src_dir, "MeasureUnitGenerated.h") + with io.open(sanctioned_h_file, mode="w", encoding="utf-8", newline="") as f: + println = partial(print, file=f) + + println(generatedFileWarning) + + println( + """ +#ifndef intl_components_MeasureUnitGenerated_h +#define intl_components_MeasureUnitGenerated_h + +namespace mozilla::intl { + +struct SimpleMeasureUnit { + const char* const type; + const char* const name; +}; + +/** + * The list of currently supported simple unit identifiers. + * + * The list must be kept in alphabetical order of |name|. + */ +inline constexpr SimpleMeasureUnit simpleMeasureUnits[] = { + // clang-format off""" + ) + + for unit_name in sorted(sanctioned_units): + println(' {{"{}", "{}"}},'.format(find_unit_type(unit_name), unit_name)) + + println( + """ + // clang-format on +}; + +} // namespace mozilla::intl + +#endif +""".strip( + "\n" + ) + ) + + writeUnitTestFiles(all_units, sanctioned_units) + + +def writeUnitTestFiles(all_units, sanctioned_units): + """Generate test files for unit number formatters.""" + + js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__)) + test_dir = os.path.join( + js_src_builtin_intl_dir, "../../tests/non262/Intl/NumberFormat" + ) + + def write_test(file_name, test_content, indent=4): + file_path = os.path.join(test_dir, file_name) + with io.open(file_path, mode="w", encoding="utf-8", newline="") as f: + println = partial(print, file=f) + + println('// |reftest| skip-if(!this.hasOwnProperty("Intl"))') + println("") + println(generatedFileWarning) + println("") + + sanctioned_units_array = json.dumps( + [unit for unit in sorted(sanctioned_units)], + indent=indent, + separators=(",", ": "), + ) + + println( + "const sanctionedSimpleUnitIdentifiers = {};".format( + sanctioned_units_array + ) + ) + + println(test_content) + + println( + """ +if (typeof reportCompare === "function") +{}reportCompare(true, true);""".format( + " " * indent + ) + ) + + write_test( + "unit-compound-combinations.js", + """ +// Test all simple unit identifier combinations are allowed. + +for (const numerator of sanctionedSimpleUnitIdentifiers) { + for (const denominator of sanctionedSimpleUnitIdentifiers) { + const unit = `${numerator}-per-${denominator}`; + const nf = new Intl.NumberFormat("en", {style: "unit", unit}); + + assertEq(nf.format(1), nf.formatToParts(1).map(p => p.value).join("")); + } +}""", + ) + + all_units_array = json.dumps( + ["-".join(unit) for unit in sorted(all_units)], indent=4, separators=(",", ": ") + ) + + write_test( + "unit-well-formed.js", + """ +const allUnits = {}; +""".format( + all_units_array + ) + + """ +// Test only sanctioned unit identifiers are allowed. + +for (const typeAndUnit of allUnits) { + const [_, type, unit] = typeAndUnit.match(/(\w+)-(.+)/); + + let allowed; + if (unit.includes("-per-")) { + const [numerator, denominator] = unit.split("-per-"); + allowed = sanctionedSimpleUnitIdentifiers.includes(numerator) && + sanctionedSimpleUnitIdentifiers.includes(denominator); + } else { + allowed = sanctionedSimpleUnitIdentifiers.includes(unit); + } + + if (allowed) { + const nf = new Intl.NumberFormat("en", {style: "unit", unit}); + assertEq(nf.format(1), nf.formatToParts(1).map(p => p.value).join("")); + } else { + assertThrowsInstanceOf(() => new Intl.NumberFormat("en", {style: "unit", unit}), + RangeError, `Missing error for "${typeAndUnit}"`); + } +}""", + ) + + write_test( + "unit-formatToParts-has-unit-field.js", + """ +// Test only English and Chinese to keep the overall runtime reasonable. +// +// Chinese is included because it contains more than one "unit" element for +// certain unit combinations. +const locales = ["en", "zh"]; + +// Plural rules for English only differentiate between "one" and "other". Plural +// rules for Chinese only use "other". That means we only need to test two values +// per unit. +const values = [0, 1]; + +// Ensure unit formatters contain at least one "unit" element. + +for (const locale of locales) { + for (const unit of sanctionedSimpleUnitIdentifiers) { + const nf = new Intl.NumberFormat(locale, {style: "unit", unit}); + + for (const value of values) { + assertEq(nf.formatToParts(value).some(e => e.type === "unit"), true, + `locale=${locale}, unit=${unit}`); + } + } + + for (const numerator of sanctionedSimpleUnitIdentifiers) { + for (const denominator of sanctionedSimpleUnitIdentifiers) { + const unit = `${numerator}-per-${denominator}`; + const nf = new Intl.NumberFormat(locale, {style: "unit", unit}); + + for (const value of values) { + assertEq(nf.formatToParts(value).some(e => e.type === "unit"), true, + `locale=${locale}, unit=${unit}`); + } + } + } +}""", + indent=2, + ) + + +def updateUnits(topsrcdir, args): + js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__)) + icu_path = os.path.join(topsrcdir, "intl", "icu") + icu_unit_path = os.path.join(icu_path, "source", "data", "unit") + + with io.open( + os.path.join(js_src_builtin_intl_dir, "SanctionedSimpleUnitIdentifiers.yaml"), + mode="r", + encoding="utf-8", + ) as f: + sanctioned_units = yaml.safe_load(f) + + # Read all possible ICU unit identifiers from the "unit/root.txt" resource. + unit_root_file = os.path.join(icu_unit_path, "root.txt") + all_units = readICUUnitResourceFile(unit_root_file) + + # Compute the set of effectively supported ICU unit identifiers. + supported_units = computeSupportedUnits(all_units, sanctioned_units) + + # Read the list of units we're including into the ICU data file. + data_filter_file = os.path.join(icu_path, "data_filter.json") + filtered_units = readICUDataFilterForUnits(data_filter_file) + + # Both sets must match to avoid resource loading errors at runtime. + if supported_units != filtered_units: + + def units_to_string(units): + return ", ".join("/".join(u) for u in units) + + missing = supported_units - filtered_units + if missing: + raise RuntimeError("Missing units: {}".format(units_to_string(missing))) + + # Not exactly an error, but we currently don't have a use case where we need to support + # more units than required by ECMA-402. + extra = filtered_units - supported_units + if extra: + raise RuntimeError("Unnecessary units: {}".format(units_to_string(extra))) + + writeSanctionedSimpleUnitIdentifiersFiles(all_units, sanctioned_units) + + +def readICUNumberingSystemsResourceFile(filepath): + """Returns a dictionary of numbering systems where the key denotes the numbering system name + and the value a dictionary with additional numbering system data. + + Example: + + numberingSystems:table(nofallback){ + numberingSystems{ + latn{ + algorithmic:int{0} + desc{"0123456789"} + radix:int{10} + } + roman{ + algorithmic:int{1} + desc{"%roman-upper"} + radix:int{10} + } + } + } + + Returns {"latn": {"digits": "0123456789", "algorithmic": False}, + "roman": {"algorithmic": True}} + """ + + start_table_re = re.compile(r"^(\w+)(?:\:[\w\(\)]+)?\{$") + end_table_re = re.compile(r"^\}$") + table_entry_re = re.compile(r"^(\w+)(?:\:[\w\(\)]+)?\{(?:(?:\"(.*?)\")|(\d+))\}$") + + # The current resource table. + table = {} + + # List of parent tables when parsing. + parents = [] + + # Track multi-line comments state. + in_multiline_comment = False + + for line in flines(filepath, "utf-8-sig"): + # Remove leading and trailing whitespace. + line = line.strip() + + # Skip over comments. + if in_multiline_comment: + if line.endswith("*/"): + in_multiline_comment = False + continue + + if line.startswith("//"): + continue + + if line.startswith("/*"): + in_multiline_comment = True + continue + + # Try to match the start of a table, e.g. `latn{`. + match = start_table_re.match(line) + if match: + parents.append(table) + table_name = match.group(1) + new_table = {} + table[table_name] = new_table + table = new_table + continue + + # Try to match the end of a table. + match = end_table_re.match(line) + if match: + table = parents.pop() + continue + + # Try to match a table entry, e.g. `desc{"0123456789"}`. + match = table_entry_re.match(line) + if match: + entry_key = match.group(1) + entry_value = ( + match.group(2) if match.group(2) is not None else int(match.group(3)) + ) + table[entry_key] = entry_value + continue + + raise Exception("unexpected line: '{}' in {}".format(line, filepath)) + + assert len(parents) == 0, "Not all tables closed" + assert len(table) == 1, "More than one root table" + + # Remove the two top-level "numberingSystems" tables. + (_, numbering_systems) = table.popitem() + (_, numbering_systems) = numbering_systems.popitem() + + # Assert all numbering systems use base 10. + assert all(ns["radix"] == 10 for ns in numbering_systems.values()) + + # Return the numbering systems. + return { + key: {"digits": value["desc"], "algorithmic": False} + if not bool(value["algorithmic"]) + else {"algorithmic": True} + for (key, value) in numbering_systems.items() + } + + +def writeNumberingSystemFiles(numbering_systems): + js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__)) + + numbering_systems_js_file = os.path.join( + js_src_builtin_intl_dir, "NumberingSystemsGenerated.h" + ) + with io.open( + numbering_systems_js_file, mode="w", encoding="utf-8", newline="" + ) as f: + println = partial(print, file=f) + + println(generatedFileWarning) + + println( + """ +/** + * The list of numbering systems with simple digit mappings. + */ + +#ifndef builtin_intl_NumberingSystemsGenerated_h +#define builtin_intl_NumberingSystemsGenerated_h +""" + ) + + simple_numbering_systems = sorted( + name + for (name, value) in numbering_systems.items() + if not value["algorithmic"] + ) + + println("// clang-format off") + println("#define NUMBERING_SYSTEMS_WITH_SIMPLE_DIGIT_MAPPINGS \\") + println( + "{}".format( + ", \\\n".join( + ' "{}"'.format(name) for name in simple_numbering_systems + ) + ) + ) + println("// clang-format on") + println("") + + println("#endif // builtin_intl_NumberingSystemsGenerated_h") + + js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__)) + test_dir = os.path.join(js_src_builtin_intl_dir, "../../tests/non262/Intl") + + intl_shell_js_file = os.path.join(test_dir, "shell.js") + + with io.open(intl_shell_js_file, mode="w", encoding="utf-8", newline="") as f: + println = partial(print, file=f) + + println(generatedFileWarning) + + println( + """ +// source: CLDR file common/bcp47/number.xml; version CLDR {}. +// https://github.com/unicode-org/cldr/blob/master/common/bcp47/number.xml +// https://github.com/unicode-org/cldr/blob/master/common/supplemental/numberingSystems.xml +""".format( + readCLDRVersionFromICU() + ).rstrip() + ) + + numbering_systems_object = json.dumps( + numbering_systems, + indent=2, + separators=(",", ": "), + sort_keys=True, + ensure_ascii=False, + ) + println("const numberingSystems = {};".format(numbering_systems_object)) + + +def updateNumberingSystems(topsrcdir, args): + js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__)) + icu_path = os.path.join(topsrcdir, "intl", "icu") + icu_misc_path = os.path.join(icu_path, "source", "data", "misc") + + with io.open( + os.path.join(js_src_builtin_intl_dir, "NumberingSystems.yaml"), + mode="r", + encoding="utf-8", + ) as f: + numbering_systems = yaml.safe_load(f) + + # Read all possible ICU unit identifiers from the "misc/numberingSystems.txt" resource. + misc_ns_file = os.path.join(icu_misc_path, "numberingSystems.txt") + all_numbering_systems = readICUNumberingSystemsResourceFile(misc_ns_file) + + all_numbering_systems_simple_digits = { + name + for (name, value) in all_numbering_systems.items() + if not value["algorithmic"] + } + + # Assert ICU includes support for all required numbering systems. If this assertion fails, + # something is broken in ICU. + assert all_numbering_systems_simple_digits.issuperset( + numbering_systems + ), "{}".format(numbering_systems.difference(all_numbering_systems_simple_digits)) + + # Assert the spec requires support for all numbering systems with simple digit mappings. If + # this assertion fails, file a PR at <https://github.com/tc39/ecma402> to include any new + # numbering systems. + assert all_numbering_systems_simple_digits.issubset(numbering_systems), "{}".format( + all_numbering_systems_simple_digits.difference(numbering_systems) + ) + + writeNumberingSystemFiles(all_numbering_systems) + + +if __name__ == "__main__": + import argparse + + # This script must reside in js/src/builtin/intl to work correctly. + (thisDir, thisFile) = os.path.split(os.path.abspath(__file__)) + dirPaths = os.path.normpath(thisDir).split(os.sep) + if "/".join(dirPaths[-4:]) != "js/src/builtin/intl": + raise RuntimeError("%s must reside in js/src/builtin/intl" % __file__) + topsrcdir = "/".join(dirPaths[:-4]) + + def EnsureHttps(v): + if not v.startswith("https:"): + raise argparse.ArgumentTypeError("URL protocol must be https: " % v) + return v + + parser = argparse.ArgumentParser(description="Update intl data.") + subparsers = parser.add_subparsers(help="Select update mode") + + parser_cldr_tags = subparsers.add_parser( + "langtags", help="Update CLDR language tags data" + ) + parser_cldr_tags.add_argument( + "--version", metavar="VERSION", help="CLDR version number" + ) + parser_cldr_tags.add_argument( + "--url", + metavar="URL", + default="https://unicode.org/Public/cldr/<VERSION>/cldr-common-<VERSION>.0.zip", + type=EnsureHttps, + help="Download url CLDR data (default: %(default)s)", + ) + parser_cldr_tags.add_argument( + "--out", + default=os.path.join( + topsrcdir, "intl", "components", "src", "LocaleGenerated.cpp" + ), + help="Output file (default: %(default)s)", + ) + parser_cldr_tags.add_argument( + "file", nargs="?", help="Local cldr-common.zip file, if omitted uses <URL>" + ) + parser_cldr_tags.set_defaults(func=updateCLDRLangTags) + + parser_tz = subparsers.add_parser("tzdata", help="Update tzdata") + parser_tz.add_argument( + "--tz", + help="Local tzdata directory or file, if omitted downloads tzdata " + "distribution from https://www.iana.org/time-zones/", + ) + # ICU doesn't include the backzone file by default, but we still like to + # use the backzone time zone names to avoid user confusion. This does lead + # to formatting "historic" dates (pre-1970 era) with the wrong time zone, + # but that's probably acceptable for now. + parser_tz.add_argument( + "--ignore-backzone", + action="store_true", + help="Ignore tzdata's 'backzone' file. Can be enabled to generate more " + "accurate time zone canonicalization reflecting the actual time " + "zones as used by ICU.", + ) + parser_tz.add_argument( + "--out", + default=os.path.join(thisDir, "TimeZoneDataGenerated.h"), + help="Output file (default: %(default)s)", + ) + parser_tz.set_defaults(func=partial(updateTzdata, topsrcdir)) + + parser_currency = subparsers.add_parser( + "currency", help="Update currency digits mapping" + ) + parser_currency.add_argument( + "--url", + metavar="URL", + default="https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-one.xml", # NOQA: E501 + type=EnsureHttps, + help="Download url for the currency & funds code list (default: " + "%(default)s)", + ) + parser_currency.add_argument( + "--out", + default=os.path.join(thisDir, "CurrencyDataGenerated.js"), + help="Output file (default: %(default)s)", + ) + parser_currency.add_argument( + "file", nargs="?", help="Local currency code list file, if omitted uses <URL>" + ) + parser_currency.set_defaults(func=partial(updateCurrency, topsrcdir)) + + parser_units = subparsers.add_parser( + "units", help="Update sanctioned unit identifiers mapping" + ) + parser_units.set_defaults(func=partial(updateUnits, topsrcdir)) + + parser_numbering_systems = subparsers.add_parser( + "numbering", help="Update numbering systems with simple digit mappings" + ) + parser_numbering_systems.set_defaults( + func=partial(updateNumberingSystems, topsrcdir) + ) + + args = parser.parse_args() + args.func(args) |