Commit 837e374e authored by littledan's avatar littledan Committed by Commit bot

Separate String.prototype.replace into RegExp.prototype[Symbol.replace]

This patch implements one aspect of ES2015 RegExp subclassing:
String.prototype.replace is separated into two parts, a method on
RegExp.prototype in case the first argument is a RegExp, and the
String.prototype.replace method, which handles the string pattern
case. This separation is described in the ES2015 specification.
Most of the patch is simply moving code from string.js to regexp.js.

R=yangguo
LOG=Y
BUG=v8:4343

Review URL: https://codereview.chromium.org/1590673002

Cr-Commit-Position: refs/heads/master@{#33393}
parent 0f041e60
......@@ -9,6 +9,7 @@
// -------------------------------------------------------------------
// Imports
var ExpandReplacement;
var FLAG_harmony_tolength;
var GlobalObject = global.Object;
var GlobalRegExp = global.RegExp;
......@@ -17,6 +18,7 @@ var InternalArray = utils.InternalArray;
var InternalPackedArray = utils.InternalPackedArray;
var MakeTypeError;
var matchSymbol = utils.ImportNow("match_symbol");
var replaceSymbol = utils.ImportNow("replace_symbol");
var searchSymbol = utils.ImportNow("search_symbol");
var splitSymbol = utils.ImportNow("split_symbol");
......@@ -25,6 +27,7 @@ utils.ImportFromExperimental(function(from) {
});
utils.Import(function(from) {
ExpandReplacement = from.ExpandReplacement;
MakeTypeError = from.MakeTypeError;
});
......@@ -379,6 +382,175 @@ function RegExpMatch(string) {
}
// ES6 21.2.5.8.
// TODO(lrn): This array will survive indefinitely if replace is never
// called again. However, it will be empty, since the contents are cleared
// in the finally block.
var reusableReplaceArray = new InternalArray(4);
// Helper function for replacing regular expressions with the result of a
// function application in String.prototype.replace.
function StringReplaceGlobalRegExpWithFunction(subject, regexp, replace) {
var resultArray = reusableReplaceArray;
if (resultArray) {
reusableReplaceArray = null;
} else {
// Inside a nested replace (replace called from the replacement function
// of another replace) or we have failed to set the reusable array
// back due to an exception in a replacement function. Create a new
// array to use in the future, or until the original is written back.
resultArray = new InternalArray(16);
}
var res = %RegExpExecMultiple(regexp,
subject,
RegExpLastMatchInfo,
resultArray);
regexp.lastIndex = 0;
if (IS_NULL(res)) {
// No matches at all.
reusableReplaceArray = resultArray;
return subject;
}
var len = res.length;
if (NUMBER_OF_CAPTURES(RegExpLastMatchInfo) == 2) {
// If the number of captures is two then there are no explicit captures in
// the regexp, just the implicit capture that captures the whole match. In
// this case we can simplify quite a bit and end up with something faster.
// The builder will consist of some integers that indicate slices of the
// input string and some replacements that were returned from the replace
// function.
var match_start = 0;
for (var i = 0; i < len; i++) {
var elem = res[i];
if (%_IsSmi(elem)) {
// Integers represent slices of the original string.
if (elem > 0) {
match_start = (elem >> 11) + (elem & 0x7ff);
} else {
match_start = res[++i] - elem;
}
} else {
var func_result = replace(elem, match_start, subject);
// Overwrite the i'th element in the results with the string we got
// back from the callback function.
res[i] = TO_STRING(func_result);
match_start += elem.length;
}
}
} else {
for (var i = 0; i < len; i++) {
var elem = res[i];
if (!%_IsSmi(elem)) {
// elem must be an Array.
// Use the apply argument as backing for global RegExp properties.
var func_result = %Apply(replace, UNDEFINED, elem, 0, elem.length);
// Overwrite the i'th element in the results with the string we got
// back from the callback function.
res[i] = TO_STRING(func_result);
}
}
}
var result = %StringBuilderConcat(res, len, subject);
resultArray.length = 0;
reusableReplaceArray = resultArray;
return result;
}
// Compute the string of a given regular expression capture.
function CaptureString(string, lastCaptureInfo, index) {
// Scale the index.
var scaled = index << 1;
// Compute start and end.
var start = lastCaptureInfo[CAPTURE(scaled)];
// If start isn't valid, return undefined.
if (start < 0) return;
var end = lastCaptureInfo[CAPTURE(scaled + 1)];
return %_SubString(string, start, end);
}
function StringReplaceNonGlobalRegExpWithFunction(subject, regexp, replace) {
var matchInfo = DoRegExpExec(regexp, subject, 0);
if (IS_NULL(matchInfo)) {
regexp.lastIndex = 0;
return subject;
}
var index = matchInfo[CAPTURE0];
var result = %_SubString(subject, 0, index);
var endOfMatch = matchInfo[CAPTURE1];
// Compute the parameter list consisting of the match, captures, index,
// and subject for the replace function invocation.
// The number of captures plus one for the match.
var m = NUMBER_OF_CAPTURES(matchInfo) >> 1;
var replacement;
if (m == 1) {
// No captures, only the match, which is always valid.
var s = %_SubString(subject, index, endOfMatch);
// Don't call directly to avoid exposing the built-in global object.
replacement = replace(s, index, subject);
} else {
var parameters = new InternalArray(m + 2);
for (var j = 0; j < m; j++) {
parameters[j] = CaptureString(subject, matchInfo, j);
}
parameters[j] = index;
parameters[j + 1] = subject;
replacement = %Apply(replace, UNDEFINED, parameters, 0, j + 2);
}
result += replacement; // The add method converts to string if necessary.
// Can't use matchInfo any more from here, since the function could
// overwrite it.
return result + %_SubString(subject, endOfMatch, subject.length);
}
function RegExpReplace(string, replace) {
// TODO(littledan): allow non-regexp receivers.
if (!IS_REGEXP(this)) {
throw MakeTypeError(kIncompatibleMethodReceiver,
"RegExp.prototype.@@replace", this);
}
var subject = TO_STRING(string);
var search = this;
if (!IS_CALLABLE(replace)) {
replace = TO_STRING(replace);
if (!REGEXP_GLOBAL(search)) {
// Non-global regexp search, string replace.
var match = DoRegExpExec(search, subject, 0);
if (match == null) {
search.lastIndex = 0
return subject;
}
if (replace.length == 0) {
return %_SubString(subject, 0, match[CAPTURE0]) +
%_SubString(subject, match[CAPTURE1], subject.length)
}
return ExpandReplacement(replace, subject, RegExpLastMatchInfo,
%_SubString(subject, 0, match[CAPTURE0])) +
%_SubString(subject, match[CAPTURE1], subject.length);
}
// Global regexp search, string replace.
search.lastIndex = 0;
return %StringReplaceGlobalRegExpWithString(
subject, search, replace, RegExpLastMatchInfo);
}
if (REGEXP_GLOBAL(search)) {
// Global regexp search, function replace.
return StringReplaceGlobalRegExpWithFunction(subject, search, replace);
}
// Non-global regexp search, function replace.
return StringReplaceNonGlobalRegExpWithFunction(subject, search, replace);
}
// ES6 21.2.5.9.
function RegExpSearch(string) {
// TODO(yangguo): allow non-regexp receivers.
......@@ -511,6 +683,7 @@ utils.InstallFunctions(GlobalRegExp.prototype, DONT_ENUM, [
"toString", RegExpToString,
"compile", RegExpCompileJS,
matchSymbol, RegExpMatch,
replaceSymbol, RegExpReplace,
searchSymbol, RegExpSearch,
splitSymbol, RegExpSplit,
]);
......
......@@ -20,9 +20,8 @@ var MakeTypeError;
var MathMax;
var MathMin;
var matchSymbol = utils.ImportNow("match_symbol");
var RegExpExec;
var RegExpExecNoTests;
var RegExpLastMatchInfo;
var replaceSymbol = utils.ImportNow("replace_symbol");
var searchSymbol = utils.ImportNow("search_symbol");
var splitSymbol = utils.ImportNow("split_symbol");
......@@ -33,9 +32,7 @@ utils.Import(function(from) {
MakeTypeError = from.MakeTypeError;
MathMax = from.MathMax;
MathMin = from.MathMin;
RegExpExec = from.RegExpExec;
RegExpExecNoTests = from.RegExpExecNoTests;
RegExpLastMatchInfo = from.RegExpLastMatchInfo;
});
//-------------------------------------------------------------------
......@@ -206,14 +203,12 @@ function StringNormalizeJS() {
var reusableMatchInfo = [2, "", "", -1, -1];
// ECMA-262, section 15.5.4.11
// ES6, section 21.1.3.14
function StringReplace(search, replace) {
CHECK_OBJECT_COERCIBLE(this, "String.prototype.replace");
var subject = TO_STRING(this);
// Decision tree for dispatch
// .. regexp search
// .. regexp search (in src/js/regexp.js, RegExpReplace)
// .... string replace
// ...... non-global search
// ........ empty string replace
......@@ -229,39 +224,14 @@ function StringReplace(search, replace) {
// ...... function replace
// ...... string replace (with $-expansion)
if (IS_REGEXP(search)) {
if (!IS_CALLABLE(replace)) {
replace = TO_STRING(replace);
if (!REGEXP_GLOBAL(search)) {
// Non-global regexp search, string replace.
var match = RegExpExec(search, subject, 0);
if (match == null) {
search.lastIndex = 0
return subject;
}
if (replace.length == 0) {
return %_SubString(subject, 0, match[CAPTURE0]) +
%_SubString(subject, match[CAPTURE1], subject.length)
}
return ExpandReplacement(replace, subject, RegExpLastMatchInfo,
%_SubString(subject, 0, match[CAPTURE0])) +
%_SubString(subject, match[CAPTURE1], subject.length);
if (!IS_NULL_OR_UNDEFINED(search)) {
var replacer = search[replaceSymbol];
if (!IS_UNDEFINED(replacer)) {
return %_Call(replacer, search, this, replace);
}
// Global regexp search, string replace.
search.lastIndex = 0;
return %StringReplaceGlobalRegExpWithString(
subject, search, replace, RegExpLastMatchInfo);
}
if (REGEXP_GLOBAL(search)) {
// Global regexp search, function replace.
return StringReplaceGlobalRegExpWithFunction(subject, search, replace);
}
// Non-global regexp search, function replace.
return StringReplaceNonGlobalRegExpWithFunction(subject, search, replace);
}
var subject = TO_STRING(this);
search = TO_STRING(search);
......@@ -379,130 +349,6 @@ function ExpandReplacement(string, subject, matchInfo, result) {
}
// Compute the string of a given regular expression capture.
function CaptureString(string, lastCaptureInfo, index) {
// Scale the index.
var scaled = index << 1;
// Compute start and end.
var start = lastCaptureInfo[CAPTURE(scaled)];
// If start isn't valid, return undefined.
if (start < 0) return;
var end = lastCaptureInfo[CAPTURE(scaled + 1)];
return %_SubString(string, start, end);
}
// TODO(lrn): This array will survive indefinitely if replace is never
// called again. However, it will be empty, since the contents are cleared
// in the finally block.
var reusableReplaceArray = new InternalArray(4);
// Helper function for replacing regular expressions with the result of a
// function application in String.prototype.replace.
function StringReplaceGlobalRegExpWithFunction(subject, regexp, replace) {
var resultArray = reusableReplaceArray;
if (resultArray) {
reusableReplaceArray = null;
} else {
// Inside a nested replace (replace called from the replacement function
// of another replace) or we have failed to set the reusable array
// back due to an exception in a replacement function. Create a new
// array to use in the future, or until the original is written back.
resultArray = new InternalArray(16);
}
var res = %RegExpExecMultiple(regexp,
subject,
RegExpLastMatchInfo,
resultArray);
regexp.lastIndex = 0;
if (IS_NULL(res)) {
// No matches at all.
reusableReplaceArray = resultArray;
return subject;
}
var len = res.length;
if (NUMBER_OF_CAPTURES(RegExpLastMatchInfo) == 2) {
// If the number of captures is two then there are no explicit captures in
// the regexp, just the implicit capture that captures the whole match. In
// this case we can simplify quite a bit and end up with something faster.
// The builder will consist of some integers that indicate slices of the
// input string and some replacements that were returned from the replace
// function.
var match_start = 0;
for (var i = 0; i < len; i++) {
var elem = res[i];
if (%_IsSmi(elem)) {
// Integers represent slices of the original string.
if (elem > 0) {
match_start = (elem >> 11) + (elem & 0x7ff);
} else {
match_start = res[++i] - elem;
}
} else {
var func_result = replace(elem, match_start, subject);
// Overwrite the i'th element in the results with the string we got
// back from the callback function.
res[i] = TO_STRING(func_result);
match_start += elem.length;
}
}
} else {
for (var i = 0; i < len; i++) {
var elem = res[i];
if (!%_IsSmi(elem)) {
// elem must be an Array.
// Use the apply argument as backing for global RegExp properties.
var func_result = %Apply(replace, UNDEFINED, elem, 0, elem.length);
// Overwrite the i'th element in the results with the string we got
// back from the callback function.
res[i] = TO_STRING(func_result);
}
}
}
var result = %StringBuilderConcat(res, len, subject);
resultArray.length = 0;
reusableReplaceArray = resultArray;
return result;
}
function StringReplaceNonGlobalRegExpWithFunction(subject, regexp, replace) {
var matchInfo = RegExpExec(regexp, subject, 0);
if (IS_NULL(matchInfo)) {
regexp.lastIndex = 0;
return subject;
}
var index = matchInfo[CAPTURE0];
var result = %_SubString(subject, 0, index);
var endOfMatch = matchInfo[CAPTURE1];
// Compute the parameter list consisting of the match, captures, index,
// and subject for the replace function invocation.
// The number of captures plus one for the match.
var m = NUMBER_OF_CAPTURES(matchInfo) >> 1;
var replacement;
if (m == 1) {
// No captures, only the match, which is always valid.
var s = %_SubString(subject, index, endOfMatch);
// Don't call directly to avoid exposing the built-in global object.
replacement = replace(s, index, subject);
} else {
var parameters = new InternalArray(m + 2);
for (var j = 0; j < m; j++) {
parameters[j] = CaptureString(subject, matchInfo, j);
}
parameters[j] = index;
parameters[j + 1] = subject;
replacement = %Apply(replace, UNDEFINED, parameters, 0, j + 2);
}
result += replacement; // The add method converts to string if necessary.
// Can't use matchInfo any more from here, since the function could
// overwrite it.
return result + %_SubString(subject, endOfMatch, subject.length);
}
// ES6 21.1.3.15.
function StringSearch(pattern) {
CHECK_OBJECT_COERCIBLE(this, "String.prototype.search");
......@@ -1098,6 +944,7 @@ utils.InstallFunctions(GlobalString.prototype, DONT_ENUM, [
// Exports
utils.Export(function(to) {
to.ExpandReplacement = ExpandReplacement;
to.StringCharAt = StringCharAtJS;
to.StringIndexOf = StringIndexOfJS;
to.StringLastIndexOf = StringLastIndexOfJS;
......
// Copyright 2015 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-regexp-subclass
var pattern = {
[Symbol.replace]: (string, newValue) => string + newValue
};
// Check object coercible fails.
assertThrows(() => String.prototype.replace.call(null, pattern, "x"),
TypeError);
// Override is called.
assertEquals("abcdex", "abcde".replace(pattern, "x"));
// Non-callable override.
pattern[Symbol.replace] = "dumdidum";
assertThrows(() => "abcde".replace(pattern, "x"), TypeError);
assertEquals("[Symbol.replace]", RegExp.prototype[Symbol.replace].name);
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