Bug 1540021: Implement String.prototype.replaceAll proposal. r=jorendorff
authorAndré Bargull <andre.bargull@gmail.com>
Thu, 07 Nov 2019 18:20:15 +0000
changeset 501148 705533ca2ccbc551379eea7c783280bd353244fa
parent 501147 1a256ac8701b41e441315f46ff0a87347d1cc74b
child 501149 a8d5b5b537cebc5b2de484cfa5b5d340ef9cb474
push id36781
push usercsabou@mozilla.com
push dateFri, 08 Nov 2019 05:21:04 +0000
treeherdermozilla-central@dff542b772e5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjorendorff
bugs1540021
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1540021: Implement String.prototype.replaceAll proposal. r=jorendorff This change doesn't include the String.prototype.matchAll modifications, because those are already part of the main spec. MCallOptimize.cpp: - The inlining is more conservative than `inlineIsRegExpObject`, because it's not clear at this point we need the extra features from `inlineIsRegExpObject`. String.js: - The self-hosted part is slightly different than the current spec text, because it combines the match and replace loops. The non-functional replace part is implemented in C++, so we can reuse the existing C++ matcher functions. String.cpp: - Added some extra assertions to `AppendDollarReplacement` and also had to change the `infallibleAppend` call into a normal `append` call, because when called from `replaceAll`, we may not have reserved enough space in the StringBuffer. - `replaceAll` has a specialised implementation when the pattern is the empty string, because in that case the pattern is interleaved in-between each character, so we don't need to find the next match and can also directly reserve the correct string length (when no '$' characters are present in the replacement string). This should allow users to update from the previous `str.split("").join(r)` pattern to `str.replaceAll("", r)` without loss of performance. - When the pattern isn't the empty string, we reuse the existing `StringMatch` and `AppendDollarReplacement` functions to match and replace the pattern. This feature is still restricted to Nightly, because no test262 tests are currently available. Differential Revision: https://phabricator.services.mozilla.com/D51842
js/src/builtin/RegExp.js
js/src/builtin/String.cpp
js/src/builtin/String.h
js/src/builtin/String.js
js/src/jit/InlinableNatives.h
js/src/jit/IonBuilder.h
js/src/jit/MCallOptimize.cpp
js/src/js.msg
js/src/tests/non262/String/replaceAll.js
js/src/vm/SelfHosting.cpp
--- a/js/src/builtin/RegExp.js
+++ b/js/src/builtin/RegExp.js
@@ -1305,8 +1305,28 @@ function RegExpStringIteratorNext() {
         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[std_match];
+
+    // Step 3.
+    if (matcher !== undefined) {
+        return !!matcher;
+    }
+
+    // Steps 4-5.
+    return IsPossiblyWrappedRegExpObject(argument);
+}
--- a/js/src/builtin/String.cpp
+++ b/js/src/builtin/String.cpp
@@ -13,16 +13,17 @@
 #include "mozilla/PodOperations.h"
 #include "mozilla/Range.h"
 #include "mozilla/TextUtils.h"
 #include "mozilla/TypeTraits.h"
 #include "mozilla/Unused.h"
 
 #include <limits>
 #include <string.h>
+#include <type_traits>
 
 #include "jsapi.h"
 #include "jsnum.h"
 #include "jstypes.h"
 #include "jsutil.h"
 
 #include "builtin/Array.h"
 #include "builtin/Boolean.h"
@@ -2756,19 +2757,23 @@ static JSString* BuildFlatRopeReplacemen
 }
 
 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.
-  newReplaceChars.infallibleAppend(repChars, firstDollarIndex);
+  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;
       }
@@ -3037,16 +3042,265 @@ JSString* js::str_replace_string_raw(JSC
       return nullptr;
     }
   } else if (string->isRope()) {
     return BuildFlatRopeReplacement(cx, string, repl, match, patternLength);
   }
   return BuildFlatReplacement(cx, string, repl, match, patternLength);
 }
 
+// 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.
+  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 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;
+  }
+
+  // Step 11 (moved below).
+
+  // Step 12.
+  uint32_t endOfLastMatch = 0;
+
+  // Step 13.
+  JSStringBuilder result(cx);
+  if (std::is_same<StrChar, char16_t>::value ||
+      std::is_same<RepChar, char16_t>::value) {
+    if (!result.ensureTwoByteChars()) {
+      return nullptr;
+    }
+  }
+
+  {
+    AutoCheckCannotGC nogc;
+    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 nullptr;
+      }
+    }
+
+    do {
+      // Step 14.c.
+      // Append the substring before the current match.
+      if (!result.append(strChars + endOfLastMatch,
+                         position - endOfLastMatch)) {
+        return nullptr;
+      }
+
+      // 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 nullptr;
+        }
+      } else {
+        if (!result.append(repChars, replaceLength)) {
+          return nullptr;
+        }
+      }
+
+      // 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.
+    if (!result.append(strChars + endOfLastMatch,
+                       stringLength - endOfLastMatch)) {
+      return nullptr;
+    }
+  }
+
+  // Step 16.
+  return result.finishString();
+}
+
+// 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.
+  const size_t stringLength = string->length();
+  const size_t replaceLength = replaceString->length();
+
+  // 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 (std::is_same<StrChar, char16_t>::value ||
+      std::is_same<RepChar, char16_t>::value) {
+    if (!result.ensureTwoByteChars()) {
+      return nullptr;
+    }
+  }
+
+  {
+    AutoCheckCannotGC nogc;
+    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 nullptr;
+      }
+    } 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 nullptr;
+      }
+
+      if (!result.reserve(length.value())) {
+        return nullptr;
+      }
+    }
+
+    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 nullptr;
+      }
+
+      // Step 14.c.
+      if (!result.append(strChars[index])) {
+        return nullptr;
+      }
+    }
+
+    // Steps 11, 14.a-b and 14.d.
+    // The empty string also matches at the end of the string.
+    if (!appendReplacement(stringLength)) {
+      return nullptr;
+    }
+
+    // Step 15 (not applicable when searchString is the empty string).
+  }
+
+  // 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;
+  }
+
+  RootedLinearString str(cx, string->ensureLinear(cx));
+  if (!str) {
+    return nullptr;
+  }
+
+  RootedLinearString repl(cx, replaceString->ensureLinear(cx));
+  if (!repl) {
+    return nullptr;
+  }
+
+  RootedLinearString 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* NewFullyAllocatedStringArray(JSContext* cx,
                                                  HandleObjectGroup group,
                                                  uint32_t length) {
   ArrayObject* array = NewFullyAllocatedArrayTryUseGroup(cx, group, length);
   if (!array) {
     return nullptr;
   }
 
@@ -3392,16 +3646,19 @@ static const JSFunctionSpec string_metho
     JS_FN("normalize", str_normalize, 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),
+#ifdef NIGHTLY_BUILD
+    JS_SELF_HOSTED_FN("replaceAll", "String_replaceAll", 2, 0),
+#endif
     JS_SELF_HOSTED_FN("split", "String_split", 2, 0),
     JS_SELF_HOSTED_FN("substr", "String_substr", 2, 0),
 
     /* Python-esque sequence methods. */
     JS_FN("concat", str_concat, 1, 0),
     JS_SELF_HOSTED_FN("slice", "String_slice", 2, 0),
 
     /* HTML string methods. */
--- a/js/src/builtin/String.h
+++ b/js/src/builtin/String.h
@@ -83,16 +83,20 @@ ArrayObject* StringSplitString(JSContext
 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 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);
 
--- a/js/src/builtin/String.js
+++ b/js/src/builtin/String.js
@@ -212,16 +212,122 @@ function String_replace(searchValue, rep
     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.
+    RequireObjectCoercible(this);
+
+    // Step 2.
+    if (searchValue !== undefined && searchValue !== null) {
+        // Steps 2.a-b.
+        if (IsRegExp(searchValue)) {
+            // Step 2.b.i.
+            var flags = searchValue.flags;
+
+            // Step 2.b.ii.
+            if (flags === undefined || flags === null) {
+                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, std_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 !(std_search in StringProto);
 }
 
--- a/js/src/jit/InlinableNatives.h
+++ b/js/src/jit/InlinableNatives.h
@@ -73,16 +73,17 @@
   _(MathCbrt)                                      \
                                                    \
   _(ReflectGetPrototypeOf)                         \
                                                    \
   _(RegExpMatcher)                                 \
   _(RegExpSearcher)                                \
   _(RegExpTester)                                  \
   _(IsRegExpObject)                                \
+  _(IsPossiblyWrappedRegExpObject)                 \
   _(RegExpPrototypeOptimizable)                    \
   _(RegExpInstanceOptimizable)                     \
   _(GetFirstDollarIndex)                           \
                                                    \
   _(String)                                        \
   _(StringCharCodeAt)                              \
   _(StringFromCharCode)                            \
   _(StringFromCodePoint)                           \
--- a/js/src/jit/IonBuilder.h
+++ b/js/src/jit/IonBuilder.h
@@ -781,16 +781,17 @@ class IonBuilder : public MIRGenerator,
   // Reflect natives.
   InliningResult inlineReflectGetPrototypeOf(CallInfo& callInfo);
 
   // RegExp intrinsics.
   InliningResult inlineRegExpMatcher(CallInfo& callInfo);
   InliningResult inlineRegExpSearcher(CallInfo& callInfo);
   InliningResult inlineRegExpTester(CallInfo& callInfo);
   InliningResult inlineIsRegExpObject(CallInfo& callInfo);
+  InliningResult inlineIsPossiblyWrappedRegExpObject(CallInfo& callInfo);
   InliningResult inlineRegExpPrototypeOptimizable(CallInfo& callInfo);
   InliningResult inlineRegExpInstanceOptimizable(CallInfo& callInfo);
   InliningResult inlineGetFirstDollarIndex(CallInfo& callInfo);
 
   // Object natives and intrinsics.
   InliningResult inlineObject(CallInfo& callInfo);
   InliningResult inlineObjectCreate(CallInfo& callInfo);
   InliningResult inlineObjectIs(CallInfo& callInfo);
--- a/js/src/jit/MCallOptimize.cpp
+++ b/js/src/jit/MCallOptimize.cpp
@@ -111,16 +111,17 @@ static bool CanInlineCrossRealm(Inlinabl
 
     case InlinableNative::IntlGuardToCollator:
     case InlinableNative::IntlGuardToDateTimeFormat:
     case InlinableNative::IntlGuardToListFormat:
     case InlinableNative::IntlGuardToNumberFormat:
     case InlinableNative::IntlGuardToPluralRules:
     case InlinableNative::IntlGuardToRelativeTimeFormat:
     case InlinableNative::IsRegExpObject:
+    case InlinableNative::IsPossiblyWrappedRegExpObject:
     case InlinableNative::RegExpMatcher:
     case InlinableNative::RegExpSearcher:
     case InlinableNative::RegExpTester:
     case InlinableNative::RegExpPrototypeOptimizable:
     case InlinableNative::RegExpInstanceOptimizable:
     case InlinableNative::GetFirstDollarIndex:
     case InlinableNative::IntrinsicNewArrayIterator:
     case InlinableNative::IntrinsicNewStringIterator:
@@ -417,16 +418,18 @@ IonBuilder::InliningResult IonBuilder::i
     case InlinableNative::RegExpMatcher:
       return inlineRegExpMatcher(callInfo);
     case InlinableNative::RegExpSearcher:
       return inlineRegExpSearcher(callInfo);
     case InlinableNative::RegExpTester:
       return inlineRegExpTester(callInfo);
     case InlinableNative::IsRegExpObject:
       return inlineIsRegExpObject(callInfo);
+    case InlinableNative::IsPossiblyWrappedRegExpObject:
+      return inlineIsPossiblyWrappedRegExpObject(callInfo);
     case InlinableNative::RegExpPrototypeOptimizable:
       return inlineRegExpPrototypeOptimizable(callInfo);
     case InlinableNative::RegExpInstanceOptimizable:
       return inlineRegExpInstanceOptimizable(callInfo);
     case InlinableNative::GetFirstDollarIndex:
       return inlineGetFirstDollarIndex(callInfo);
     case InlinableNative::IntrinsicNewRegExpStringIterator:
       return inlineNewIterator(callInfo, MNewIterator::RegExpStringIterator);
@@ -2505,16 +2508,53 @@ IonBuilder::InliningResult IonBuilder::i
     current->add(hasClass);
     current->push(hasClass);
   }
 
   callInfo.setImplicitlyUsedUnchecked();
   return InliningStatus_Inlined;
 }
 
+IonBuilder::InliningResult IonBuilder::inlineIsPossiblyWrappedRegExpObject(
+    CallInfo& callInfo) {
+  MOZ_ASSERT(!callInfo.constructing());
+  MOZ_ASSERT(callInfo.argc() == 1);
+
+  if (getInlineReturnType() != MIRType::Boolean) {
+    return InliningStatus_NotInlined;
+  }
+
+  MDefinition* arg = callInfo.getArg(0);
+  if (arg->type() != MIRType::Object) {
+    return InliningStatus_NotInlined;
+  }
+
+  TemporaryTypeSet* types = arg->resultTypeSet();
+  if (!types) {
+    return InliningStatus_NotInlined;
+  }
+
+  // Don't inline if the argument might be a wrapper.
+  if (types->forAllClasses(constraints(), IsProxyClass) !=
+      TemporaryTypeSet::ForAllResult::ALL_FALSE) {
+    return InliningStatus_NotInlined;
+  }
+
+  if (const JSClass* clasp = types->getKnownClass(constraints())) {
+    pushConstant(BooleanValue(clasp == &RegExpObject::class_));
+  } else {
+    MHasClass* hasClass = MHasClass::New(alloc(), arg, &RegExpObject::class_);
+    current->add(hasClass);
+    current->push(hasClass);
+  }
+
+  callInfo.setImplicitlyUsedUnchecked();
+  return InliningStatus_Inlined;
+}
+
 IonBuilder::InliningResult IonBuilder::inlineRegExpPrototypeOptimizable(
     CallInfo& callInfo) {
   MOZ_ASSERT(!callInfo.constructing());
   MOZ_ASSERT(callInfo.argc() == 1);
 
   MDefinition* protoArg = callInfo.getArg(0);
 
   if (protoArg->type() != MIRType::Object) {
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -135,16 +135,18 @@ MSG_DEF(JSMSG_INVALID_DATE,            0
 MSG_DEF(JSMSG_BAD_TOISOSTRING_PROP,    0, JSEXN_TYPEERR, "toISOString property is not callable")
 
 // String
 MSG_DEF(JSMSG_BAD_URI,                 0, JSEXN_URIERR, "malformed URI sequence")
 MSG_DEF(JSMSG_INVALID_NORMALIZE_FORM,  0, JSEXN_RANGEERR, "form must be one of 'NFC', 'NFD', 'NFKC', or 'NFKD'")
 MSG_DEF(JSMSG_NEGATIVE_REPETITION_COUNT, 0, JSEXN_RANGEERR, "repeat count must be non-negative")
 MSG_DEF(JSMSG_NOT_A_CODEPOINT,         1, JSEXN_RANGEERR, "{0} is not a valid code point")
 MSG_DEF(JSMSG_RESULTING_STRING_TOO_LARGE, 0, JSEXN_RANGEERR, "repeat count must be less than infinity and not overflow maximum string size")
+MSG_DEF(JSMSG_FLAGS_UNDEFINED_OR_NULL, 0, JSEXN_TYPEERR, "'flags' property must neither be undefined nor null")
+MSG_DEF(JSMSG_REQUIRES_GLOBAL_REGEXP,  1, JSEXN_TYPEERR, "{0} must be called with a global RegExp")
 
 // Number
 MSG_DEF(JSMSG_BAD_RADIX,               0, JSEXN_RANGEERR, "radix must be an integer at least 2 and no greater than 36")
 MSG_DEF(JSMSG_PRECISION_RANGE,         1, JSEXN_RANGEERR, "precision {0} out of range")
 
 // Function
 MSG_DEF(JSMSG_BAD_APPLY_ARGS,          1, JSEXN_TYPEERR, "second argument to Function.prototype.{0} must be an array")
 MSG_DEF(JSMSG_BAD_FORMAL,              0, JSEXN_SYNTAXERR, "malformed formal parameter")
new file mode 100644
--- /dev/null
+++ b/js/src/tests/non262/String/replaceAll.js
@@ -0,0 +1,217 @@
+// |reftest| skip-if(!String.prototype.replaceAll)
+
+function neverCalled() {
+  assertEq(true, false, "unexpected call");
+}
+
+const g = newGlobal();
+
+assertEq(typeof String.prototype.replaceAll, "function");
+assertEq(String.prototype.replaceAll.length, 2);
+assertEq(String.prototype.replaceAll.name, "replaceAll");
+
+// Throws if called with undefined or null.
+assertThrowsInstanceOf(() => String.prototype.replaceAll.call(undefined), TypeError);
+assertThrowsInstanceOf(() => String.prototype.replaceAll.call(null), TypeError);
+
+// Throws if called with a non-global RegExp.
+assertThrowsInstanceOf(() => "".replaceAll(/a/, ""), TypeError);
+assertThrowsInstanceOf(() => "".replaceAll(g.RegExp(""), ""), TypeError);
+
+// Also throws with RegExp-like objects.
+assertThrowsInstanceOf(() => {
+  "".replaceAll({[Symbol.match]: neverCalled, flags: ""}, "");
+}, TypeError);
+
+// |flags| property mustn't be undefined or null.
+assertThrowsInstanceOf(() => {
+  "".replaceAll({[Symbol.match]: neverCalled, flags: undefined}, "");
+}, TypeError);
+assertThrowsInstanceOf(() => {
+  "".replaceAll({[Symbol.match]: neverCalled, flags: null}, "");
+}, TypeError);
+
+// Global RegExp (or RegExp-like) simply redirect to @@replace.
+assertEq("aba".replace(/a/g, "c"), "cbc");
+assertEq("aba".replace(g.RegExp("a", "g"), "c"), "cbc");
+assertEq("aba".replace({
+  [Symbol.match]: true,
+  [Symbol.replace]: () => "ok",
+  flags: "flags has 'g' character",
+}, ""), "ok");
+
+// Applies ToString on the replace-function return value.
+assertEq("aa".replaceAll("a", () => ({toString(){ return 1; }})), "11");
+assertEq("aa".replaceAll("a", () => ({valueOf(){ return 1; }})), "[object Object][object Object]");
+
+const replacer = {
+  "$$": function(searchString, position, string) {
+    "use strict";
+    assertEq(this, undefined);
+
+    return "$";
+  },
+  "$$-$$": function(searchString, position, string) {
+    "use strict";
+    assertEq(this, undefined);
+
+    return "$-$";
+  },
+  "$&": function(searchString, position, string) {
+    "use strict";
+    assertEq(this, undefined);
+
+    return string.substring(position, position + searchString.length);
+  },
+  "$&-$&": function(searchString, position, string) {
+    "use strict";
+    assertEq(this, undefined);
+
+    var s = string.substring(position, position + searchString.length);
+    return `${s}-${s}`;
+  },
+  "$`": function(searchString, position, string) {
+    "use strict";
+    assertEq(this, undefined);
+
+    return string.substring(0, position);
+  },
+  "$`-$`": function(searchString, position, string) {
+    "use strict";
+    assertEq(this, undefined);
+
+    var s = string.substring(0, position);
+    return `${s}-${s}`;
+  },
+  "$'": function(searchString, position, string) {
+    "use strict";
+    assertEq(this, undefined);
+
+    return string.substring(position + searchString.length);
+  },
+  "$'-$'": function(searchString, position, string) {
+    "use strict";
+    assertEq(this, undefined);
+
+    var s = string.substring(position + searchString.length);
+    return `${s}-${s}`;
+  },
+  "A": function(searchString, position, string) {
+    "use strict";
+    assertEq(this, undefined);
+
+    return "A";
+  },
+  "A-B": function(searchString, position, string) {
+    "use strict";
+    assertEq(this, undefined);
+
+    return "A-B";
+  },
+  "": function(searchString, position, string) {
+    "use strict";
+    assertEq(this, undefined);
+
+    return "";
+  },
+};
+
+// Tests when |pattern| is longer than |string|.
+{
+  const tests = [
+    { string: "", pattern: "a" },
+    { string: "a", pattern: "ab" },
+    { string: "", pattern: "α" },
+    { string: "α", pattern: "αβ" },
+  ];
+
+  for (let [replacementString, replacementFunction] of Object.entries(replacer)) {
+    for (let {string, pattern} of tests) {
+      let a = string.replaceAll(pattern, replacementString);
+      let b = string.replaceAll(pattern, replacementFunction);
+      let expected = string.replace(RegExp(pattern, "g"), replacementString);
+      assertEq(a, expected);
+      assertEq(b, expected);
+      assertEq(expected, string);
+    }
+  }
+}
+
+// Tests when |pattern| doesn't match once.
+ {
+   const tests = [
+    { string: "a", pattern: "A" },
+    { string: "ab", pattern: "A" },
+    { string: "ab", pattern: "AB" },
+
+    { string: "α", pattern: "Γ" },
+    { string: "αβ", pattern: "Γ" },
+    { string: "αβ", pattern: "ΓΔ" },
+  ];
+
+  for (let [replacementString, replacementFunction] of Object.entries(replacer)) {
+    for (let {string, pattern} of tests) {
+      let a = string.replaceAll(pattern, replacementString);
+      let b = string.replaceAll(pattern, replacementFunction);
+      let expected = string.replace(RegExp(pattern, "g"), replacementString);
+      assertEq(a, expected);
+      assertEq(b, expected);
+      assertEq(expected, string);
+    }
+  }
+}
+
+// Tests when |pattern| is the empty string.
+{
+  const strings = ["", "a", "ab", "α", "αβ"];
+  const pattern = "";
+  const re = /(?:)/g;
+
+  for (let [replacementString, replacementFunction] of Object.entries(replacer)) {
+    for (let string of strings) {
+      let a = string.replaceAll(pattern, replacementString);
+      let b = string.replaceAll(pattern, replacementFunction);
+      let expected = string.replace(re, replacementString);
+      assertEq(a, expected);
+      assertEq(b, expected);
+    }
+  }
+}
+
+// Tests when |pattern| isn't the empty string.
+{
+  const tests = [
+    {
+      strings: [
+        "a", "b",
+        "aa", "ab", "ba", "bb",
+        "aaa", "aab", "aba", "abb", "baa", "bab", "bba", "bbb",
+      ],
+      pattern: "a",
+    },
+    {
+      strings: [
+        "α", "β",
+        "αα", "αβ", "βα", "ββ",
+        "ααα", "ααβ", "αβα", "αββ", "βαα", "βαβ", "ββα", "βββ",
+      ],
+      pattern: "α",
+    },
+  ];
+
+  for (let {strings, pattern} of tests) {
+    let re = RegExp(pattern, "g");
+    for (let [replacementString, replacementFunction] of Object.entries(replacer)) {
+      for (let string of strings) {
+        let a = string.replaceAll(pattern, replacementString);
+        let b = string.replaceAll(pattern, replacementFunction);
+        let expected = string.replace(re, replacementString);
+        assertEq(a, expected);
+        assertEq(b, expected);
+      }
+    }
+  }
+}
+
+if (typeof reportCompare === "function")
+  reportCompare(true, true);
\ No newline at end of file
--- a/js/src/vm/SelfHosting.cpp
+++ b/js/src/vm/SelfHosting.cpp
@@ -1564,16 +1564,34 @@ static bool intrinsic_StringReplaceStrin
   if (!result) {
     return false;
   }
 
   args.rval().setString(result);
   return true;
 }
 
+static bool intrinsic_StringReplaceAllString(JSContext* cx, unsigned argc,
+                                             Value* vp) {
+  CallArgs args = CallArgsFromVp(argc, vp);
+  MOZ_ASSERT(args.length() == 3);
+
+  RootedString string(cx, args[0].toString());
+  RootedString pattern(cx, args[1].toString());
+  RootedString replacement(cx, args[2].toString());
+  JSString* result =
+      str_replaceAll_string_raw(cx, string, pattern, replacement);
+  if (!result) {
+    return false;
+  }
+
+  args.rval().setString(result);
+  return true;
+}
+
 bool js::intrinsic_StringSplitString(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
   MOZ_ASSERT(args.length() == 2);
 
   RootedString string(cx, args[0].toString());
   RootedString sep(cx, args[1].toString());
 
   RootedObjectGroup group(cx, ObjectGroupRealm::getStringSplitStringGroup(cx));
@@ -2475,16 +2493,19 @@ static const JSFunctionSpec intrinsic_fu
 #endif  // ENABLE_INTL_API
 
     JS_FN("GetOwnPropertyDescriptorToArray", GetOwnPropertyDescriptorToArray, 2,
           0),
 
     JS_INLINABLE_FN("IsRegExpObject",
                     intrinsic_IsInstanceOfBuiltin<RegExpObject>, 1, 0,
                     IsRegExpObject),
+    JS_INLINABLE_FN("IsPossiblyWrappedRegExpObject",
+                    intrinsic_IsPossiblyWrappedInstanceOfBuiltin<RegExpObject>,
+                    1, 0, IsPossiblyWrappedRegExpObject),
     JS_FN("CallRegExpMethodIfWrapped",
           CallNonGenericSelfhostedMethod<Is<RegExpObject>>, 2, 0),
     JS_INLINABLE_FN("RegExpMatcher", RegExpMatcher, 3, 0, RegExpMatcher),
     JS_INLINABLE_FN("RegExpSearcher", RegExpSearcher, 3, 0, RegExpSearcher),
     JS_INLINABLE_FN("RegExpTester", RegExpTester, 3, 0, RegExpTester),
     JS_FN("RegExpCreate", intrinsic_RegExpCreate, 2, 0),
     JS_INLINABLE_FN("RegExpPrototypeOptimizable", RegExpPrototypeOptimizable, 1,
                     0, RegExpPrototypeOptimizable),
@@ -2495,16 +2516,17 @@ static const JSFunctionSpec intrinsic_fu
     JS_FN("GetStringDataProperty", intrinsic_GetStringDataProperty, 2, 0),
     JS_INLINABLE_FN("GetFirstDollarIndex", GetFirstDollarIndex, 1, 0,
                     GetFirstDollarIndex),
 
     JS_FN("FlatStringMatch", FlatStringMatch, 2, 0),
     JS_FN("FlatStringSearch", FlatStringSearch, 2, 0),
     JS_INLINABLE_FN("StringReplaceString", intrinsic_StringReplaceString, 3, 0,
                     IntrinsicStringReplaceString),
+    JS_FN("StringReplaceAllString", intrinsic_StringReplaceAllString, 3, 0),
     JS_INLINABLE_FN("StringSplitString", intrinsic_StringSplitString, 2, 0,
                     IntrinsicStringSplitString),
     JS_FN("StringSplitStringLimit", intrinsic_StringSplitStringLimit, 3, 0),
     JS_FN("ThrowArgTypeNotObject", intrinsic_ThrowArgTypeNotObject, 2, 0),
 
     // See builtin/RegExp.h for descriptions of the regexp_* functions.
     JS_FN("regexp_construct_raw_flags", regexp_construct_raw_flags, 2, 0),