diff options
-rw-r--r-- | js/src/builtin/RegExp.js | 21 | ||||
-rw-r--r-- | js/src/builtin/String.js | 109 | ||||
-rw-r--r-- | js/src/jit/InlinableNatives.h | 1 | ||||
-rw-r--r-- | js/src/jit/IonBuilder.h | 1 | ||||
-rw-r--r-- | js/src/jit/MCallOptimize.cpp | 40 | ||||
-rw-r--r-- | js/src/jsstr.cpp | 261 | ||||
-rw-r--r-- | js/src/jsstr.h | 4 | ||||
-rw-r--r-- | js/src/vm/SelfHosting.cpp | 66 |
8 files changed, 478 insertions, 25 deletions
diff --git a/js/src/builtin/RegExp.js b/js/src/builtin/RegExp.js index 91212066c8..7218fc0e8a 100644 --- a/js/src/builtin/RegExp.js +++ b/js/src/builtin/RegExp.js @@ -1166,3 +1166,24 @@ function RegExpStringIteratorNext() { result.value = match; return result; } + +// String.prototype.matchAll proposal. +// 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); +} diff --git a/js/src/builtin/String.js b/js/src/builtin/String.js index d07ec6127b..c70f8558be 100644 --- a/js/src/builtin/String.js +++ b/js/src/builtin/String.js @@ -224,6 +224,115 @@ function String_generic_replace(thisValue, searchValue, replaceValue) { return callFunction(String_replace, thisValue, 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 String_generic_replace(thisValue, searchValue, replaceValue) { + if (thisValue === undefined) + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.replace'); + return callFunction(String_replace, thisValue, searchValue, replaceValue); +} + function StringProtoHasNoSearch() { var ObjectProto = GetBuiltinPrototype("Object"); var StringProto = GetBuiltinPrototype("String"); diff --git a/js/src/jit/InlinableNatives.h b/js/src/jit/InlinableNatives.h index e4fdf7ee2f..01b5f78522 100644 --- a/js/src/jit/InlinableNatives.h +++ b/js/src/jit/InlinableNatives.h @@ -66,6 +66,7 @@ _(RegExpSearcher) \ _(RegExpTester) \ _(IsRegExpObject) \ + _(IntrinsicIsPossiblyWrappedRegExpObject)\ _(RegExpPrototypeOptimizable) \ _(RegExpInstanceOptimizable) \ _(GetFirstDollarIndex) \ diff --git a/js/src/jit/IonBuilder.h b/js/src/jit/IonBuilder.h index a262f81668..402fbbf1af 100644 --- a/js/src/jit/IonBuilder.h +++ b/js/src/jit/IonBuilder.h @@ -867,6 +867,7 @@ class IonBuilder InliningStatus inlineRegExpSearcher(CallInfo& callInfo); InliningStatus inlineRegExpTester(CallInfo& callInfo); InliningStatus inlineIsRegExpObject(CallInfo& callInfo); + InliningStatus inlineIsPossiblyWrappedRegExpObject(CallInfo& callInfo); InliningStatus inlineRegExpPrototypeOptimizable(CallInfo& callInfo); InliningStatus inlineRegExpInstanceOptimizable(CallInfo& callInfo); InliningStatus inlineGetFirstDollarIndex(CallInfo& callInfo); diff --git a/js/src/jit/MCallOptimize.cpp b/js/src/jit/MCallOptimize.cpp index 182fa2fd5a..23e894ffb2 100644 --- a/js/src/jit/MCallOptimize.cpp +++ b/js/src/jit/MCallOptimize.cpp @@ -185,6 +185,8 @@ IonBuilder::inlineNativeCall(CallInfo& callInfo, JSFunction* target) return inlineRegExpTester(callInfo); case InlinableNative::IsRegExpObject: return inlineIsRegExpObject(callInfo); + case InlinableNative::IntrinsicIsPossiblyWrappedRegExpObject: + return inlineIsPossiblyWrappedRegExpObject(callInfo); case InlinableNative::RegExpPrototypeOptimizable: return inlineRegExpPrototypeOptimizable(callInfo); case InlinableNative::RegExpInstanceOptimizable: @@ -1902,6 +1904,44 @@ IonBuilder::inlineIsRegExpObject(CallInfo& callInfo) callInfo.setImplicitlyUsedUnchecked(); return InliningStatus_Inlined; } +IonBuilder::InliningStatus +IonBuilder::inlineIsPossiblyWrappedRegExpObject(CallInfo& callInfo) +{ + MOZ_ASSERT(!callInfo.constructing()); + MOZ_ASSERT(callInfo.argc() == 1); + + if (callInfo.getArg(0)->type() != MIRType::Object) + return InliningStatus_NotInlined; + 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 Class* 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::InliningStatus IonBuilder::inlineRegExpPrototypeOptimizable(CallInfo& callInfo) diff --git a/js/src/jsstr.cpp b/js/src/jsstr.cpp index 7a08619985..6eb11b1cc1 100644 --- a/js/src/jsstr.cpp +++ b/js/src/jsstr.cpp @@ -2111,9 +2111,13 @@ AppendDollarReplacement(StringBuffer& newReplaceChars, size_t firstDollarIndex, 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; @@ -2329,6 +2333,200 @@ js::str_flat_replace_string(JSContext *cx, HandleString string, HandleString pat return str; } +// 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* str, + JSLinearString* pat, + JSLinearString* rep) { + // Step 7. + const size_t stringLength = str->length(); + const size_t searchLength = pat->length(); + const size_t replaceLength = rep->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(str, pat, 0); + + // Nothing to replace, so return early. + if (position < 0) { + return str; + } + + // Step 11 (moved below). + + // Step 12. + uint32_t endOfLastMatch = 0; + + // Step 13. + StringBuffer 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 = str->chars<StrChar>(nogc); + const RepChar* repChars = rep->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, + str, 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(str, pat, 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* str, + JSLinearString* rep) { + // Step 7. + const size_t stringLength = str->length(); + const size_t replaceLength = rep->length(); + + // Step 8 (advanceBy is 1 when searchString is the empty string). + + // Steps 9-12 (trivial when searchString is the empty string). + + // Step 13. + StringBuffer 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 = str->chars<StrChar>(nogc); + const RepChar* repChars = rep->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, + str, 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.o JSString* js::str_replace_string_raw(JSContext* cx, HandleString string, HandleString pattern, HandleString replacement) @@ -2371,6 +2569,62 @@ js::str_replace_string_raw(JSContext* cx, HandleString string, HandleString patt return BuildFlatReplacement(cx, string, repl, match, patternLength); } +JSString* +js::str_replaceAll_string_raw(JSContext* cx, HandleString string, HandleString pattern, + HandleString replacement) +{ + const size_t stringLength = string->length(); + const size_t searchLength = pattern->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, replacement->ensureLinear(cx)); + if (!repl) { + return nullptr; + } + + RootedLinearString search(cx, pattern->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); +} + // ES 2016 draft Mar 25, 2016 21.1.3.17 steps 4, 8, 12-18. static JSObject* SplitHelper(JSContext* cx, HandleLinearString str, uint32_t limit, HandleLinearString sep, @@ -2572,9 +2826,9 @@ static const JSFunctionSpec string_methods[] = { JS_FN("endsWith", str_endsWith, 1,0), JS_FN("trim", str_trim, 0,0), JS_FN("trimLeft", str_trimStart, 0,0), - JS_FN("trimStart", str_trimStart, 0,0), + JS_FN("trimStart", str_trimStart, 0,0), JS_FN("trimRight", str_trimEnd, 0,0), - JS_FN("trimEnd", str_trimEnd, 0,0), + JS_FN("trimEnd", str_trimEnd, 0,0), JS_FN("toLocaleLowerCase", str_toLocaleLowerCase, 0,0), JS_FN("toLocaleUpperCase", str_toLocaleUpperCase, 0,0), JS_SELF_HOSTED_FN("localeCompare", "String_localeCompare", 1,0), @@ -2586,6 +2840,7 @@ static const JSFunctionSpec string_methods[] = { 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), diff --git a/js/src/jsstr.h b/js/src/jsstr.h index 42891900fa..0e31276a86 100644 --- a/js/src/jsstr.h +++ b/js/src/jsstr.h @@ -476,6 +476,10 @@ 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 StringConstructor(JSContext* cx, unsigned argc, Value* vp); diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 9c20bbe9df..38785a822c 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -191,6 +191,28 @@ intrinsic_IsInstanceOfBuiltin(JSContext* cx, unsigned argc, Value* vp) template<typename T> static bool +intrinsic_IsPossiblyWrappedBuiltin(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + bool isTypeT = false; + if (args[0].isObject()) { + JSObject* obj = CheckedUnwrap(&args[0].toObject()); + if (!obj) { + JS_ReportErrorASCII(cx, "Permission denied to access object"); + return false; + } + + isTypeT = obj->is<T>(); + } + + args.rval().setBoolean(isTypeT); + return true; +} + +template<typename T> +static bool intrinsic_GuardToBuiltin(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); @@ -1173,27 +1195,6 @@ intrinsic_IsFloat32TypedArray(JSContext* cx, unsigned argc, Value* vp) } static bool -intrinsic_IsPossiblyWrappedTypedArray(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - - bool isTypedArray = false; - if (args[0].isObject()) { - JSObject* obj = CheckedUnwrap(&args[0].toObject()); - if (!obj) { - JS_ReportErrorASCII(cx, "Permission denied to access object"); - return false; - } - - isTypedArray = obj->is<TypedArrayObject>(); - } - - args.rval().setBoolean(isTypedArray); - return true; -} - -static bool intrinsic_TypedArrayBuffer(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); @@ -1756,6 +1757,24 @@ intrinsic_StringReplaceString(JSContext* cx, unsigned argc, Value* vp) 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) { @@ -2401,7 +2420,7 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_INLINABLE_FN("IsTypedArray", intrinsic_IsInstanceOfBuiltin<TypedArrayObject>, 1,0, IntrinsicIsTypedArray), - JS_INLINABLE_FN("IsPossiblyWrappedTypedArray",intrinsic_IsPossiblyWrappedTypedArray,1,0, + JS_INLINABLE_FN("IsPossiblyWrappedTypedArray",intrinsic_IsPossiblyWrappedBuiltin<TypedArrayObject>,1,0, IntrinsicIsPossiblyWrappedTypedArray), JS_FN("TypedArrayBuffer", intrinsic_TypedArrayBuffer, 1,0), @@ -2513,6 +2532,8 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_INLINABLE_FN("IsRegExpObject", intrinsic_IsInstanceOfBuiltin<RegExpObject>, 1,0, IsRegExpObject), + JS_INLINABLE_FN("IsPossiblyWrappedRegExpObject", intrinsic_IsPossiblyWrappedBuiltin<RegExpObject>,1,0, + IntrinsicIsPossiblyWrappedRegExpObject), JS_FN("CallRegExpMethodIfWrapped", CallNonGenericSelfhostedMethod<Is<RegExpObject>>, 2,0), JS_INLINABLE_FN("RegExpMatcher", RegExpMatcher, 4,0, @@ -2536,6 +2557,7 @@ static const JSFunctionSpec intrinsic_functions[] = { 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), |