summaryrefslogtreecommitdiffstats
path: root/js/src/builtin
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/builtin')
-rw-r--r--js/src/builtin/.eslintrc.js157
-rw-r--r--js/src/builtin/Array-inl.h40
-rw-r--r--js/src/builtin/Array.cpp5562
-rw-r--r--js/src/builtin/Array.h247
-rw-r--r--js/src/builtin/Array.js1561
-rw-r--r--js/src/builtin/AsyncFunction.js19
-rw-r--r--js/src/builtin/AsyncIteration.js594
-rw-r--r--js/src/builtin/AtomicsObject.cpp1078
-rw-r--r--js/src/builtin/AtomicsObject.h141
-rw-r--r--js/src/builtin/BigInt.cpp234
-rw-r--r--js/src/builtin/BigInt.h53
-rw-r--r--js/src/builtin/BigInt.js36
-rw-r--r--js/src/builtin/Boolean-inl.h29
-rw-r--r--js/src/builtin/Boolean.cpp178
-rw-r--r--js/src/builtin/Boolean.h23
-rw-r--r--js/src/builtin/DataViewObject.cpp1038
-rw-r--r--js/src/builtin/DataViewObject.h169
-rw-r--r--js/src/builtin/Date.js172
-rw-r--r--js/src/builtin/Error.js37
-rw-r--r--js/src/builtin/Eval.cpp547
-rw-r--r--js/src/builtin/Eval.h34
-rw-r--r--js/src/builtin/FinalizationRegistryObject.cpp849
-rw-r--r--js/src/builtin/FinalizationRegistryObject.h274
-rw-r--r--js/src/builtin/Generator.js114
-rw-r--r--js/src/builtin/HandlerFunction-inl.h110
-rw-r--r--js/src/builtin/Iterator.js785
-rw-r--r--js/src/builtin/JSON.cpp1383
-rw-r--r--js/src/builtin/JSON.h40
-rw-r--r--js/src/builtin/Map.js133
-rw-r--r--js/src/builtin/MapObject.cpp1970
-rw-r--r--js/src/builtin/MapObject.h469
-rw-r--r--js/src/builtin/ModuleObject.cpp2548
-rw-r--r--js/src/builtin/ModuleObject.h443
-rw-r--r--js/src/builtin/Number.js105
-rw-r--r--js/src/builtin/Object.cpp2308
-rw-r--r--js/src/builtin/Object.h66
-rw-r--r--js/src/builtin/Object.js369
-rw-r--r--js/src/builtin/Profilers.cpp566
-rw-r--r--js/src/builtin/Profilers.h88
-rw-r--r--js/src/builtin/Promise-inl.h45
-rw-r--r--js/src/builtin/Promise.cpp6632
-rw-r--r--js/src/builtin/Promise.h267
-rw-r--r--js/src/builtin/Promise.js78
-rw-r--r--js/src/builtin/RecordObject.cpp77
-rw-r--r--js/src/builtin/RecordObject.h31
-rw-r--r--js/src/builtin/Reflect.cpp234
-rw-r--r--js/src/builtin/Reflect.h33
-rw-r--r--js/src/builtin/Reflect.js182
-rw-r--r--js/src/builtin/ReflectParse.cpp3801
-rw-r--r--js/src/builtin/RegExp.cpp2369
-rw-r--r--js/src/builtin/RegExp.h178
-rw-r--r--js/src/builtin/RegExp.js1574
-rw-r--r--js/src/builtin/RegExpGlobalReplaceOpt.h.js174
-rw-r--r--js/src/builtin/RegExpLocalReplaceOpt.h.js164
-rw-r--r--js/src/builtin/SelfHostingDefines.h125
-rw-r--r--js/src/builtin/Set.js597
-rw-r--r--js/src/builtin/ShadowRealm.cpp688
-rw-r--r--js/src/builtin/ShadowRealm.h38
-rw-r--r--js/src/builtin/Sorting.js215
-rw-r--r--js/src/builtin/String.cpp4617
-rw-r--r--js/src/builtin/String.h106
-rw-r--r--js/src/builtin/String.js1203
-rw-r--r--js/src/builtin/Symbol.cpp235
-rw-r--r--js/src/builtin/Symbol.h71
-rw-r--r--js/src/builtin/TestingFunctions.cpp9817
-rw-r--r--js/src/builtin/TestingFunctions.h44
-rw-r--r--js/src/builtin/TestingUtility.cpp256
-rw-r--r--js/src/builtin/TestingUtility.h64
-rw-r--r--js/src/builtin/Tuple.js711
-rw-r--r--js/src/builtin/TupleObject.cpp102
-rw-r--r--js/src/builtin/TupleObject.h34
-rw-r--r--js/src/builtin/TypedArray.js2268
-rw-r--r--js/src/builtin/TypedArrayConstants.h24
-rw-r--r--js/src/builtin/Utilities.js242
-rw-r--r--js/src/builtin/WeakMap.js28
-rw-r--r--js/src/builtin/WeakMapObject-inl.h65
-rw-r--r--js/src/builtin/WeakMapObject.cpp309
-rw-r--r--js/src/builtin/WeakMapObject.h65
-rw-r--r--js/src/builtin/WeakRefObject.cpp265
-rw-r--r--js/src/builtin/WeakRefObject.h43
-rw-r--r--js/src/builtin/WeakSet.js22
-rw-r--r--js/src/builtin/WeakSetObject.cpp237
-rw-r--r--js/src/builtin/WeakSetObject.h50
-rw-r--r--js/src/builtin/WrappedFunctionObject.cpp339
-rw-r--r--js/src/builtin/WrappedFunctionObject.h43
-rw-r--r--js/src/builtin/embedjs.py204
-rw-r--r--js/src/builtin/intl/Collator.cpp472
-rw-r--r--js/src/builtin/intl/Collator.h104
-rw-r--r--js/src/builtin/intl/Collator.js468
-rw-r--r--js/src/builtin/intl/CommonFunctions.cpp149
-rw-r--r--js/src/builtin/intl/CommonFunctions.h103
-rw-r--r--js/src/builtin/intl/CommonFunctions.js992
-rw-r--r--js/src/builtin/intl/CurrencyDataGenerated.js78
-rw-r--r--js/src/builtin/intl/DateTimeFormat.cpp1567
-rw-r--r--js/src/builtin/intl/DateTimeFormat.h188
-rw-r--r--js/src/builtin/intl/DateTimeFormat.js1008
-rw-r--r--js/src/builtin/intl/DecimalNumber.cpp263
-rw-r--r--js/src/builtin/intl/DecimalNumber.h117
-rw-r--r--js/src/builtin/intl/DisplayNames.cpp551
-rw-r--r--js/src/builtin/intl/DisplayNames.h79
-rw-r--r--js/src/builtin/intl/DisplayNames.js418
-rw-r--r--js/src/builtin/intl/FormatBuffer.h155
-rw-r--r--js/src/builtin/intl/IcuMemoryUsage.java260
-rw-r--r--js/src/builtin/intl/IntlObject.cpp910
-rw-r--r--js/src/builtin/intl/IntlObject.h82
-rw-r--r--js/src/builtin/intl/IntlObject.js81
-rw-r--r--js/src/builtin/intl/LanguageTag.cpp193
-rw-r--r--js/src/builtin/intl/LanguageTag.h91
-rw-r--r--js/src/builtin/intl/ListFormat.cpp373
-rw-r--r--js/src/builtin/intl/ListFormat.h69
-rw-r--r--js/src/builtin/intl/ListFormat.js330
-rw-r--r--js/src/builtin/intl/Locale.cpp1517
-rw-r--r--js/src/builtin/intl/Locale.h61
-rw-r--r--js/src/builtin/intl/NumberFormat.cpp1374
-rw-r--r--js/src/builtin/intl/NumberFormat.h129
-rw-r--r--js/src/builtin/intl/NumberFormat.js1210
-rw-r--r--js/src/builtin/intl/NumberingSystems.yaml82
-rw-r--r--js/src/builtin/intl/NumberingSystemsGenerated.h83
-rw-r--r--js/src/builtin/intl/PluralRules.cpp423
-rw-r--r--js/src/builtin/intl/PluralRules.h98
-rw-r--r--js/src/builtin/intl/PluralRules.js390
-rw-r--r--js/src/builtin/intl/RelativeTimeFormat.cpp403
-rw-r--r--js/src/builtin/intl/RelativeTimeFormat.h87
-rw-r--r--js/src/builtin/intl/RelativeTimeFormat.js329
-rw-r--r--js/src/builtin/intl/SanctionedSimpleUnitIdentifiers.yaml58
-rw-r--r--js/src/builtin/intl/SanctionedSimpleUnitIdentifiersGenerated.js55
-rw-r--r--js/src/builtin/intl/SharedIntlData.cpp754
-rw-r--r--js/src/builtin/intl/SharedIntlData.h335
-rw-r--r--js/src/builtin/intl/StringAsciiChars.h77
-rw-r--r--js/src/builtin/intl/TimeZoneDataGenerated.h142
-rwxr-xr-xjs/src/builtin/intl/make_intl_data.py4139
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,
+ &currentNamedCapture)) {
+ 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,
+ &currentNamedCapture)) {
+ 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, &regexp->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, '"', "&quot;");
+}
+
+// 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(), &param, &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)