| // 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-regexp-gen.h' |
| |
| namespace regexp { |
| |
| extern builtin |
| SubString(implicit context: Context)(String, Smi, Smi): String; |
| |
| extern runtime RegExpExecMultiple(implicit context: Context)( |
| JSRegExp, String, RegExpMatchInfo, JSArray): Null|JSArray; |
| extern transitioning runtime |
| RegExpReplaceRT(Context, JSReceiver, String, Object): String; |
| extern transitioning runtime |
| StringBuilderConcat(implicit context: Context)(JSArray, Smi, String): String; |
| extern transitioning runtime |
| StringReplaceNonGlobalRegExpWithFunction(implicit context: Context)( |
| String, JSRegExp, Callable): String; |
| |
| transitioning macro RegExpReplaceCallableNoExplicitCaptures( |
| implicit context: Context)( |
| matchesElements: FixedArray, matchesLength: intptr, string: String, |
| replaceFn: Callable) { |
| let matchStart: Smi = 0; |
| for (let i: intptr = 0; i < matchesLength; i++) { |
| typeswitch (matchesElements.objects[i]) { |
| // Element represents a slice. |
| case (elSmi: Smi): { |
| // The slice's match start and end is either encoded as one or two |
| // smis. A positive smi indicates a single smi encoding (see |
| // ReplacementStringBuilder::AddSubjectSlice()). |
| if (elSmi > 0) { |
| // For single smi encoding, see |
| // StringBuilderSubstringLength::encode() and |
| // StringBuilderSubstringPosition::encode(). |
| const elInt: intptr = Convert<intptr>(elSmi); |
| const newMatchStart: intptr = (elInt >> 11) + (elInt & 0x7FF); |
| matchStart = Convert<Smi>(newMatchStart); |
| } else { |
| // For two smi encoding, the length is negative followed by the |
| // match start. |
| const nextEl: Smi = UnsafeCast<Smi>(matchesElements.objects[++i]); |
| matchStart = nextEl - elSmi; |
| } |
| } |
| // Element represents the matched substring, which is then passed to the |
| // replace function. |
| case (elString: String): { |
| const replacementObj: JSAny = |
| Call(context, replaceFn, Undefined, elString, matchStart, string); |
| const replacement: String = ToString_Inline(replacementObj); |
| matchesElements.objects[i] = replacement; |
| matchStart += elString.length_smi; |
| } |
| case (Object): deferred { |
| unreachable; |
| } |
| } |
| } |
| } |
| |
| transitioning macro |
| RegExpReplaceCallableWithExplicitCaptures(implicit context: Context)( |
| matchesElements: FixedArray, matchesLength: intptr, replaceFn: Callable) { |
| for (let i: intptr = 0; i < matchesLength; i++) { |
| const elArray = |
| Cast<JSArray>(matchesElements.objects[i]) otherwise continue; |
| |
| // The JSArray is expanded into the function args by Reflect.apply(). |
| // TODO(jgruber): Remove indirection through Call->ReflectApply. |
| const replacementObj: JSAny = Call( |
| context, GetReflectApply(), Undefined, replaceFn, Undefined, elArray); |
| |
| // Overwrite the i'th element in the results with the string |
| // we got back from the callback function. |
| matchesElements.objects[i] = ToString_Inline(replacementObj); |
| } |
| } |
| |
| transitioning macro RegExpReplaceFastGlobalCallable(implicit context: Context)( |
| regexp: FastJSRegExp, string: String, replaceFn: Callable): String { |
| regexp.lastIndex = 0; |
| |
| const kInitialCapacity: intptr = 16; |
| const kInitialLength: Smi = 0; |
| const result: Null|JSArray = RegExpExecMultiple( |
| regexp, string, GetRegExpLastMatchInfo(), |
| AllocateJSArray( |
| ElementsKind::PACKED_ELEMENTS, GetFastPackedElementsJSArrayMap(), |
| kInitialCapacity, kInitialLength)); |
| |
| regexp.lastIndex = 0; |
| |
| // If no matches, return the subject string. |
| if (result == Null) return string; |
| |
| const matches: JSArray = UnsafeCast<JSArray>(result); |
| const matchesLength: Smi = Cast<Smi>(matches.length) otherwise unreachable; |
| const matchesLengthInt: intptr = Convert<intptr>(matchesLength); |
| const matchesElements: FixedArray = UnsafeCast<FixedArray>(matches.elements); |
| |
| // Reload last match info since it might have changed. |
| const nofCaptures: Smi = GetRegExpLastMatchInfo().NumberOfCaptures(); |
| |
| // 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. |
| if (nofCaptures == 2) { |
| RegExpReplaceCallableNoExplicitCaptures( |
| matchesElements, matchesLengthInt, string, replaceFn); |
| } else { |
| RegExpReplaceCallableWithExplicitCaptures( |
| matchesElements, matchesLengthInt, replaceFn); |
| } |
| |
| return StringBuilderConcat(matches, matchesLength, string); |
| } |
| |
| transitioning macro RegExpReplaceFastString(implicit context: Context)( |
| regexp: JSRegExp, string: String, replaceString: String): String { |
| // The fast path is reached only if {receiver} is an unmodified JSRegExp |
| // instance, {replace_value} is non-callable, and ToString({replace_value}) |
| // does not contain '$', i.e. we're doing a simple string replacement. |
| let result: String = kEmptyString; |
| let lastMatchEnd: Smi = 0; |
| let unicode: bool = false; |
| const replaceLength: Smi = replaceString.length_smi; |
| const fastRegexp = UnsafeCast<FastJSRegExp>(regexp); |
| const global: bool = fastRegexp.global; |
| |
| if (global) { |
| unicode = fastRegexp.unicode; |
| fastRegexp.lastIndex = 0; |
| } |
| |
| while (true) { |
| const match: RegExpMatchInfo = |
| RegExpPrototypeExecBodyWithoutResultFast(regexp, string) |
| otherwise break; |
| const matchStart: Smi = match.GetStartOfCapture(0); |
| const matchEnd: Smi = match.GetEndOfCapture(0); |
| |
| // TODO(jgruber): We could skip many of the checks that using SubString |
| // here entails. |
| result = result + SubString(string, lastMatchEnd, matchStart); |
| lastMatchEnd = matchEnd; |
| |
| if (replaceLength != 0) result = result + replaceString; |
| |
| // Non-global case ends here after the first replacement. |
| if (!global) break; |
| |
| // If match is the empty string, we have to increment lastIndex. |
| if (matchEnd == matchStart) { |
| typeswitch (regexp) { |
| case (fastRegexp: FastJSRegExp): { |
| fastRegexp.lastIndex = |
| AdvanceStringIndexFast(string, fastRegexp.lastIndex, unicode); |
| } |
| case (Object): { |
| const lastIndex: JSAny = SlowLoadLastIndex(regexp); |
| const thisIndex: Number = ToLength_Inline(lastIndex); |
| const nextIndex: Number = |
| AdvanceStringIndexSlow(string, thisIndex, unicode); |
| SlowStoreLastIndex(regexp, nextIndex); |
| } |
| } |
| } |
| } |
| |
| return result + SubString(string, lastMatchEnd, string.length_smi); |
| } |
| |
| transitioning builtin RegExpReplace(implicit context: Context)( |
| regexp: FastJSRegExp, string: String, replaceValue: JSAny): String { |
| // TODO(pwong): Remove assert when all callers (StringPrototypeReplace) are |
| // from Torque. |
| assert(Is<FastJSRegExp>(regexp)); |
| |
| // 2. Is {replace_value} callable? |
| typeswitch (replaceValue) { |
| case (replaceFn: Callable): { |
| return regexp.global ? |
| RegExpReplaceFastGlobalCallable(regexp, string, replaceFn) : |
| StringReplaceNonGlobalRegExpWithFunction(string, regexp, replaceFn); |
| } |
| case (JSAny): { |
| const stableRegexp: JSRegExp = regexp; |
| const replaceString: String = ToString_Inline(replaceValue); |
| |
| try { |
| // ToString(replaceValue) could potentially change the shape of the |
| // RegExp object. Recheck that we are still on the fast path and bail |
| // to runtime otherwise. |
| const fastRegexp = Cast<FastJSRegExp>(stableRegexp) otherwise Runtime; |
| if (StringIndexOf( |
| replaceString, SingleCharacterStringConstant('$'), 0) != -1) { |
| goto Runtime; |
| } |
| |
| return RegExpReplaceFastString(fastRegexp, string, replaceString); |
| } label Runtime deferred { |
| return RegExpReplaceRT(context, stableRegexp, string, replaceString); |
| } |
| } |
| } |
| } |
| |
| const kRegExpReplaceCalledOnSlowRegExp: constexpr int31 |
| generates 'v8::Isolate::kRegExpReplaceCalledOnSlowRegExp'; |
| |
| transitioning javascript builtin RegExpPrototypeReplace( |
| js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny { |
| const methodName: constexpr string = 'RegExp.prototype.@@replace'; |
| |
| // RegExpPrototypeReplace is a bit of a beast - a summary of dispatch logic: |
| // |
| // if (!IsFastRegExp(receiver)) CallRuntime(RegExpReplace) |
| // if (IsCallable(replace)) { |
| // if (IsGlobal(receiver)) { |
| // // Called 'fast-path' but contains several runtime calls. |
| // RegExpReplaceFastGlobalCallable() |
| // } else { |
| // CallRuntime(StringReplaceNonGlobalRegExpWithFunction) |
| // } |
| // } else { |
| // if (replace.contains("$")) { |
| // CallRuntime(RegExpReplace) |
| // } else { |
| // RegExpReplaceFastString() |
| // } |
| // } |
| |
| const string: JSAny = arguments[0]; |
| const replaceValue: JSAny = arguments[1]; |
| |
| // Let rx be the this value. |
| // If Type(rx) is not Object, throw a TypeError exception. |
| const rx = Cast<JSReceiver>(receiver) |
| otherwise ThrowTypeError( |
| MessageTemplate::kIncompatibleMethodReceiver, methodName); |
| |
| // Let S be ? ToString(string). |
| const s = ToString_Inline(string); |
| |
| // Fast-path checks: 1. Is the {receiver} an unmodified JSRegExp instance? |
| try { |
| const fastRx: FastJSRegExp = Cast<FastJSRegExp>(rx) otherwise Runtime; |
| return RegExpReplace(fastRx, s, replaceValue); |
| } label Runtime deferred { |
| IncrementUseCounter(context, SmiConstant(kRegExpReplaceCalledOnSlowRegExp)); |
| return RegExpReplaceRT(context, rx, s, replaceValue); |
| } |
| } |
| } |