Commit 94890a0d authored by Joshua Litt's avatar Joshua Litt Committed by Commit Bot

[replaceAll] Implement String.prototype.replaceAll.

Implements TC39 String.prototype.replaceAll as a torque
builtin per the https://github.com/tc39/proposal-string-replaceall
proposal.

Note: matchAll changes were already added to V8 in https://chromium-review.googlesource.com/c/v8/v8/+/1846067

Bug: v8:9801
Change-Id: Ib8158eb39c854202d04710d6f9c33dcdd93fad93
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1877054
Commit-Queue: Joshua Litt <joshualitt@chromium.org>
Reviewed-by: 's avatarJakob Gruber <jgruber@chromium.org>
Cr-Commit-Position: refs/heads/master@{#64785}
parent 91c8be95
......@@ -984,6 +984,7 @@ torque_files = [
"src/builtins/string-iterator.tq",
"src/builtins/string-pad.tq",
"src/builtins/string-repeat.tq",
"src/builtins/string-replaceall.tq",
"src/builtins/string-slice.tq",
"src/builtins/string-startswith.tq",
"src/builtins/string-substring.tq",
......
......@@ -2828,6 +2828,21 @@ Cast<Smi|PromiseReaction>(o: Object): Smi|PromiseReaction labels CastError {
}
}
Cast<String|Callable>(implicit context: Context)(o: Object): String|
Callable labels CastError {
typeswitch (o) {
case (o: String): {
return o;
}
case (o: Callable): {
return o;
}
case (Object): {
goto CastError;
}
}
}
Cast<Zero|PromiseReaction>(implicit context: Context)(o: Object): Zero|
PromiseReaction labels CastError {
typeswitch (o) {
......@@ -3855,6 +3870,14 @@ transitioning macro GetMethod(implicit context: Context)(
return GetMethod(o, StringConstant(name)) otherwise IfNullOrUndefined;
}
transitioning macro GetMethod(implicit context: Context)(
o: JSAny, symbol: Symbol): Callable labels IfNullOrUndefined {
const value = GetProperty(o, symbol);
if (value == Undefined || value == Null) goto IfNullOrUndefined;
return Cast<Callable>(value)
otherwise ThrowTypeError(kPropertyNotFunction, value, symbol, o);
}
extern macro NumberToString(Number): String;
extern macro IsOneByteStringInstanceType(InstanceType): bool;
extern macro AllocateSeqOneByteString(uint32): String;
......
......@@ -1235,11 +1235,10 @@ TNode<Smi> StringBuiltinsAssembler::IndexOfDollarChar(Node* const context,
return dollar_ix;
}
compiler::Node* StringBuiltinsAssembler::GetSubstitution(
Node* context, Node* subject_string, Node* match_start_index,
Node* match_end_index, Node* replace_string) {
CSA_ASSERT(this, IsString(subject_string));
CSA_ASSERT(this, IsString(replace_string));
TNode<String> StringBuiltinsAssembler::GetSubstitution(
TNode<Context> context, TNode<String> subject_string,
TNode<Smi> match_start_index, TNode<Smi> match_end_index,
TNode<String> replace_string) {
CSA_ASSERT(this, TaggedIsPositiveSmi(match_start_index));
CSA_ASSERT(this, TaggedIsPositiveSmi(match_end_index));
......@@ -1273,7 +1272,7 @@ compiler::Node* StringBuiltinsAssembler::GetSubstitution(
}
BIND(&out);
return var_result.value();
return CAST(var_result.value());
}
// ES6 #sec-string.prototype.replace
......
......@@ -16,9 +16,11 @@ class StringBuiltinsAssembler : public CodeStubAssembler {
: CodeStubAssembler(state) {}
// ES#sec-getsubstitution
Node* GetSubstitution(Node* context, Node* subject_string,
Node* match_start_index, Node* match_end_index,
Node* replace_string);
TNode<String> GetSubstitution(TNode<Context> context,
TNode<String> subject_string,
TNode<Smi> match_start_index,
TNode<Smi> match_end_index,
TNode<String> replace_string);
void StringEqual_Core(SloppyTNode<String> lhs, Node* lhs_instance_type,
SloppyTNode<String> rhs, Node* rhs_instance_type,
TNode<IntPtrT> length, Label* if_equal,
......
// Copyright 2019 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include 'src/builtins/builtins-string-gen.h'
namespace string {
const kRegExpGlobalInvokedOnNonGlobal: constexpr MessageTemplate
generates 'MessageTemplate::kRegExpGlobalInvokedOnNonGlobal';
extern macro ReplaceSymbolConstant(): Symbol;
extern macro StringBuiltinsAssembler::GetSubstitution(
implicit context: Context)(String, Smi, Smi, String): String;
extern builtin
StringIndexOf(implicit context: Context)(String, String, Smi): Smi;
macro TryFastAbstractStringIndexOf(implicit context: Context)(
string: String, searchString: String, fromIndex: Smi): Smi labels Slow {
const stringLen = string.length_uintptr;
const searchLen = searchString.length_uintptr;
const directString = Cast<DirectString>(string) otherwise Slow;
const directSearchStr = Cast<DirectString>(searchString) otherwise Slow;
const fromIndexUint = Unsigned(SmiUntag(fromIndex));
for (let i: uintptr = fromIndexUint; i < stringLen; i++) {
let j = i;
let k: uintptr = 0;
while (j < stringLen && k < searchLen &&
StringCharCodeAt(directString, j) ==
StringCharCodeAt(directSearchStr, k)) {
j++;
k++;
}
if (k == searchLen) {
return SmiTag(Signed(i));
}
}
return -1;
}
macro AbstractStringIndexOf(implicit context: Context)(
string: String, searchString: String, fromIndex: Smi): Smi {
// Special case the empty string.
if (searchString.length_intptr == 0 &&
SmiUntag(fromIndex) <= string.length_intptr) {
return fromIndex;
}
try {
return TryFastAbstractStringIndexOf(string, searchString, fromIndex)
otherwise Slow;
}
label Slow {
for (let i: intptr = SmiUntag(fromIndex); i < string.length_intptr; i++) {
if (StringCompareSequence(
context, string, searchString, Convert<Number>(SmiTag(i))) ==
True) {
return SmiTag(i);
}
}
return -1;
}
}
transitioning macro
ThrowIfNotGlobal(implicit context: Context)(searchValue: JSAny): void {
let shouldThrow: bool;
typeswitch (searchValue) {
case (fastRegExp: FastJSRegExp): {
shouldThrow = !fastRegExp.global;
}
case (Object): {
const flags = GetProperty(searchValue, 'flags');
RequireObjectCoercible(flags, 'String.prototype.replaceAll');
shouldThrow =
StringIndexOf(
ToString_Inline(context, flags), StringConstant('g'), 0) == -1;
}
}
if (shouldThrow) {
ThrowTypeError(
kRegExpGlobalInvokedOnNonGlobal, 'String.prototype.replaceAll');
}
}
// https://tc39.es/ecma262/#sec-string.prototype.replaceall
transitioning javascript builtin StringPrototypeReplaceAll(
js-implicit context: Context,
receiver: JSAny)(searchValue: JSAny, replaceValue: JSAny): JSAny {
// 1. Let O be ? RequireObjectCoercible(this value).
RequireObjectCoercible(receiver, 'String.prototype.replaceAll');
// 2. If searchValue is neither undefined nor null, then
if (searchValue != Undefined && searchValue != Null) {
// a. Let isRegExp be ? IsRegExp(searchString).
// b. If isRegExp is true, then
// i. Let flags be ? Get(searchValue, "flags").
// ii. Perform ? RequireObjectCoercible(flags).
// iii. If ? ToString(flags) does not contain "g", throw a
// TypeError exception.
if (regexp::IsRegExp(searchValue)) {
ThrowIfNotGlobal(searchValue);
}
// TODO(joshualitt): We could easily add fast paths for string
// searchValues and potential FastRegExps.
// c. Let replacer be ? GetMethod(searchValue, @@replace).
// d. If replacer is not undefined, then
// i. Return ? Call(replacer, searchValue, « O, replaceValue »).
try {
const replacer = GetMethod(searchValue, ReplaceSymbolConstant())
otherwise ReplaceSymbolIsNullOrUndefined;
return Call(context, replacer, searchValue, receiver, replaceValue);
}
label ReplaceSymbolIsNullOrUndefined {}
}
// 3. Let string be ? ToString(O).
const string = ToString_Inline(context, receiver);
// 4. Let searchString be ? ToString(searchValue).
const searchString = ToString_Inline(context, searchValue);
// 5. Let functionalReplace be IsCallable(replaceValue).
let replaceValueArg = replaceValue;
const functionalReplace = TaggedIsCallable(replaceValue);
// 6. If functionalReplace is false, then
if (!functionalReplace) {
// a. Let replaceValue be ? ToString(replaceValue).
replaceValueArg = ToString_Inline(context, replaceValue);
}
// 7. Let searchLength be the length of searchString.
const searchLength = searchString.length_smi;
// 8. Let advanceBy be max(1, searchLength).
const advanceBy = SmiMax(1, searchLength);
// We combine the two loops from the spec into one to avoid
// needing a growable array.
//
// 9. Let matchPositions be a new empty List.
// 10. Let position be ! StringIndexOf(string, searchString, 0).
// 11. Repeat, while position is not -1
// a. Append position to the end of matchPositions.
// b. Let position be ! StringIndexOf(string, searchString,
// position + advanceBy).
// 12. Let endOfLastMatch be 0.
// 13. Let result be the empty string value.
// 14. For each position in matchPositions, do
let endOfLastMatch: Smi = 0;
let result: String = kEmptyString;
let position = AbstractStringIndexOf(string, searchString, 0);
while (position != -1) {
// a. If functionalReplace is true, then
// b. Else,
let replacement: String;
if (functionalReplace) {
// i. Let replacement be ? ToString(? Call(replaceValue, undefined,
// « searchString, position,
// string »).
replacement = ToString_Inline(
context,
Call(
context, UnsafeCast<Callable>(replaceValueArg), Undefined,
searchString, position, string));
} else {
// i. Assert: Type(replaceValue) is String.
const replaceValueString = UnsafeCast<String>(replaceValueArg);
// ii. Let captures be a new empty List.
// iii. Let replacement be GetSubstitution(searchString,
// string, position, captures,
// undefined, replaceValue).
// Note: Instead we just call a simpler GetSubstitution primitive.
const matchEndPosition = position + searchLength;
replacement = GetSubstitution(
string, position, matchEndPosition, replaceValueString);
}
// c. Let stringSlice be the substring of string consisting of the code
// units from endOfLastMatch (inclusive) up through position
// (exclusive).
const stringSlice = string::SubString(
string, Unsigned(SmiUntag(endOfLastMatch)),
Unsigned(SmiUntag(position)));
// d. Let result be the string-concatenation of result, stringSlice,
// and replacement.
// TODO(joshualitt): This leaves a completely degenerate ConsString tree.
// We could be smarter here.
result = result + stringSlice + replacement;
// e. Let endOfLastMatch be position + searchLength.
endOfLastMatch = position + searchLength;
position =
AbstractStringIndexOf(string, searchString, position + advanceBy);
}
// 15. If endOfLastMatch < the length of string, then
if (endOfLastMatch < string.length_smi) {
// a. Let result be the string-concatenation of result and the substring
// of string consisting of the code units from endOfLastMatch
// (inclusive) up through the final code unit of string (inclusive).
result = result +
string::SubString(
string, Unsigned(SmiUntag(endOfLastMatch)),
Unsigned(string.length_intptr));
}
// 16. Return result.
return result;
}
}
......@@ -126,6 +126,7 @@ enum class PrimitiveType { kBoolean, kNumber, kString, kSymbol };
PromiseRejectReactionJobTaskMap) \
V(prototype_string, prototype_string, PrototypeString) \
V(PrototypeInfoMap, prototype_info_map, PrototypeInfoMap) \
V(replace_symbol, replace_symbol, ReplaceSymbol) \
V(regexp_to_string, regexp_to_string, RegexpToString) \
V(resolve_string, resolve_string, ResolveString) \
V(SharedFunctionInfoMap, shared_function_info_map, SharedFunctionInfoMap) \
......
......@@ -203,10 +203,11 @@ DEFINE_IMPLICATION(harmony_import_meta, harmony_dynamic_import)
// Update bootstrapper.cc whenever adding a new feature flag.
// Features that are still work in progress (behind individual flags).
#define HARMONY_INPROGRESS_BASE(V) \
V(harmony_regexp_sequence, "RegExp Unicode sequence properties") \
V(harmony_weak_refs, "harmony weak references") \
V(harmony_regexp_match_indices, "harmony regexp match indices") \
#define HARMONY_INPROGRESS_BASE(V) \
V(harmony_string_replaceall, "harmony String.prototype.replaceAll") \
V(harmony_regexp_sequence, "RegExp Unicode sequence properties") \
V(harmony_weak_refs, "harmony weak references") \
V(harmony_regexp_match_indices, "harmony regexp match indices") \
V(harmony_top_level_await, "harmony top level await")
#ifdef V8_INTL_SUPPORT
......
......@@ -4438,6 +4438,17 @@ void Genesis::InitializeGlobal_harmony_regexp_match_indices() {
initial_map->AppendDescriptor(isolate(), &d);
}
void Genesis::InitializeGlobal_harmony_string_replaceall() {
if (!FLAG_harmony_string_replaceall) return;
Handle<JSFunction> string_fun(native_context()->string_function(), isolate());
Handle<JSObject> string_prototype(
JSObject::cast(string_fun->instance_prototype()), isolate());
SimpleInstallFunction(isolate(), string_prototype, "replaceAll",
Builtins::kStringPrototypeReplaceAll, 2, true);
}
#ifdef V8_INTL_SUPPORT
void Genesis::InitializeGlobal_harmony_intl_segmenter() {
......
// Copyright 2019 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Flags: --harmony-string-replaceall
assertEquals('a-b-c-d', 'a+b+c+d'.replaceAll('+', '-'));
assertEquals('aaaa', 'abcd'.replaceAll(/./g, 'a'));
assertEquals('', ''.replaceAll('a', 'b'));
assertEquals('b', ''.replaceAll('', 'b'));
assertEquals('_x_x_x_', 'xxx'.replaceAll('', '_'));
assertEquals('yx', 'xxx'.replaceAll('xx', 'y'));
assertEquals('xxxx', 'xx'.replaceAll('xx', '$&$&'));
assertEquals('ii', '.+*$.+*$'.replaceAll('.+*$', 'i'));
{
// Non regexp search value with replace method.
const nonRegExpSearchValue = {
[Symbol.replace]: (string, replacer) => {
assertEquals(string, 'barbar');
assertEquals(replacer, 'moo');
return 'foo'
},
toString: () => {
// Verify toString is not called.
unreachable();
}
};
assertEquals('foo', 'barbar'.replaceAll(nonRegExpSearchValue, 'moo'));
}
{
// A custom regexp with non coercible flags.
class RegExpNonCoercibleFlags extends RegExp {
constructor() {
super();
}
static get [Symbol.species]() {
return RegExp;
}
get flags() { return null; }
};
assertThrows(
() => { assertEquals(
'foo',
'barbar'.replaceAll(new RegExpNonCoercibleFlags, 'moo')); },
TypeError);
}
{
// Non regexp search value with replace property
const nonRegExpSearchValue = {
[Symbol.replace]: "doh",
toString: () => {
// Verify toString is not called.
unreachable();
}
};
assertThrows(
() => { 'barbar'.replaceAll(nonRegExpSearchValue, 'moo'); },
TypeError);
}
{
// Non callable, non string replace value.
const nonCallableNonStringReplace = {
toString: () => {
return 'boo';
},
};
assertEquals('booboo', 'moomoo'.replaceAll('moo', nonCallableNonStringReplace));
}
{
const positions = [];
assertEquals('bcb', 'aca'.replaceAll('a',
(searchString, position, string) => {
assertEquals('a', searchString);
assertEquals('aca', string);
positions.push(position);
return 'b';
}));
assertEquals(positions, [0,2]);
}
(function NonGlobalRegex() {
assertThrows(
() => { 'ab'.replaceAll(/./, '.'); },
TypeError);
assertThrows(
() => { 'ab'.replaceAll(/./y, '.'); },
TypeError);
})();
// Tests for stickiness gotcha.
assertEquals('o ppercase!', 'No Uppercase!'.replaceAll(/[A-Z]/g, ''));
assertEquals('o Uppercase?', 'No Uppercase?'.replaceAll(/[A-Z]/gy, ''));
assertEquals(' UPPERCASE!', 'NO UPPERCASE!'.replaceAll(/[A-Z]/gy, ''));
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment