1// Copyright 2019 the V8 project authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include 'src/builtins/builtins-regexp-gen.h' 6 7namespace regexp { 8 9extern builtin 10SubString(implicit context: Context)(String, Smi, Smi): String; 11 12extern runtime RegExpExecMultiple(implicit context: Context)( 13 JSRegExp, String, RegExpMatchInfo, JSArray): Null|JSArray; 14extern transitioning runtime 15RegExpReplaceRT(Context, JSReceiver, String, Object): String; 16extern transitioning runtime 17StringBuilderConcat(implicit context: Context)(JSArray, Smi, String): String; 18extern transitioning runtime 19StringReplaceNonGlobalRegExpWithFunction(implicit context: Context)( 20 String, JSRegExp, Callable): String; 21 22transitioning macro RegExpReplaceCallableNoExplicitCaptures( 23 implicit context: Context)( 24 matchesElements: FixedArray, matchesLength: intptr, string: String, 25 replaceFn: Callable) { 26 let matchStart: Smi = 0; 27 for (let i: intptr = 0; i < matchesLength; i++) { 28 typeswitch (matchesElements.objects[i]) { 29 // Element represents a slice. 30 case (elSmi: Smi): { 31 // The slice's match start and end is either encoded as one or two 32 // smis. A positive smi indicates a single smi encoding (see 33 // ReplacementStringBuilder::AddSubjectSlice()). 34 if (elSmi > 0) { 35 // For single smi encoding, see 36 // StringBuilderSubstringLength::encode() and 37 // StringBuilderSubstringPosition::encode(). 38 const elInt: intptr = Convert<intptr>(elSmi); 39 const newMatchStart: intptr = (elInt >> 11) + (elInt & 0x7FF); 40 matchStart = Convert<Smi>(newMatchStart); 41 } else { 42 // For two smi encoding, the length is negative followed by the 43 // match start. 44 const nextEl: Smi = UnsafeCast<Smi>(matchesElements.objects[++i]); 45 matchStart = nextEl - elSmi; 46 } 47 } 48 // Element represents the matched substring, which is then passed to the 49 // replace function. 50 case (elString: String): { 51 const replacementObj: JSAny = 52 Call(context, replaceFn, Undefined, elString, matchStart, string); 53 const replacement: String = ToString_Inline(replacementObj); 54 matchesElements.objects[i] = replacement; 55 matchStart += elString.length_smi; 56 } 57 case (Object): deferred { 58 unreachable; 59 } 60 } 61 } 62} 63 64transitioning macro 65RegExpReplaceCallableWithExplicitCaptures(implicit context: Context)( 66 matchesElements: FixedArray, matchesLength: intptr, replaceFn: Callable) { 67 for (let i: intptr = 0; i < matchesLength; i++) { 68 const elArray = 69 Cast<JSArray>(matchesElements.objects[i]) otherwise continue; 70 71 // The JSArray is expanded into the function args by Reflect.apply(). 72 // TODO(jgruber): Remove indirection through Call->ReflectApply. 73 const replacementObj: JSAny = Call( 74 context, GetReflectApply(), Undefined, replaceFn, Undefined, elArray); 75 76 // Overwrite the i'th element in the results with the string 77 // we got back from the callback function. 78 matchesElements.objects[i] = ToString_Inline(replacementObj); 79 } 80} 81 82transitioning macro RegExpReplaceFastGlobalCallable(implicit context: Context)( 83 regexp: FastJSRegExp, string: String, replaceFn: Callable): String { 84 regexp.lastIndex = 0; 85 86 const kInitialCapacity: intptr = 16; 87 const kInitialLength: Smi = 0; 88 const result: Null|JSArray = RegExpExecMultiple( 89 regexp, string, GetRegExpLastMatchInfo(), 90 AllocateJSArray( 91 ElementsKind::PACKED_ELEMENTS, GetFastPackedElementsJSArrayMap(), 92 kInitialCapacity, kInitialLength)); 93 94 regexp.lastIndex = 0; 95 96 // If no matches, return the subject string. 97 if (result == Null) return string; 98 99 const matches: JSArray = UnsafeCast<JSArray>(result); 100 const matchesLength: Smi = Cast<Smi>(matches.length) otherwise unreachable; 101 const matchesLengthInt: intptr = Convert<intptr>(matchesLength); 102 const matchesElements: FixedArray = UnsafeCast<FixedArray>(matches.elements); 103 104 // Reload last match info since it might have changed. 105 const nofCaptures: Smi = GetRegExpLastMatchInfo().NumberOfCaptures(); 106 107 // If the number of captures is two then there are no explicit captures in 108 // the regexp, just the implicit capture that captures the whole match. In 109 // this case we can simplify quite a bit and end up with something faster. 110 if (nofCaptures == 2) { 111 RegExpReplaceCallableNoExplicitCaptures( 112 matchesElements, matchesLengthInt, string, replaceFn); 113 } else { 114 RegExpReplaceCallableWithExplicitCaptures( 115 matchesElements, matchesLengthInt, replaceFn); 116 } 117 118 return StringBuilderConcat(matches, matchesLength, string); 119} 120 121transitioning macro RegExpReplaceFastString(implicit context: Context)( 122 regexp: JSRegExp, string: String, replaceString: String): String { 123 // The fast path is reached only if {receiver} is an unmodified JSRegExp 124 // instance, {replace_value} is non-callable, and ToString({replace_value}) 125 // does not contain '$', i.e. we're doing a simple string replacement. 126 let result: String = kEmptyString; 127 let lastMatchEnd: Smi = 0; 128 let unicode: bool = false; 129 const replaceLength: Smi = replaceString.length_smi; 130 const fastRegexp = UnsafeCast<FastJSRegExp>(regexp); 131 const global: bool = fastRegexp.global; 132 133 if (global) { 134 unicode = fastRegexp.unicode; 135 fastRegexp.lastIndex = 0; 136 } 137 138 while (true) { 139 const match: RegExpMatchInfo = 140 RegExpPrototypeExecBodyWithoutResultFast(regexp, string) 141 otherwise break; 142 const matchStart: Smi = match.GetStartOfCapture(0); 143 const matchEnd: Smi = match.GetEndOfCapture(0); 144 145 // TODO(jgruber): We could skip many of the checks that using SubString 146 // here entails. 147 result = result + SubString(string, lastMatchEnd, matchStart); 148 lastMatchEnd = matchEnd; 149 150 if (replaceLength != 0) result = result + replaceString; 151 152 // Non-global case ends here after the first replacement. 153 if (!global) break; 154 155 // If match is the empty string, we have to increment lastIndex. 156 if (matchEnd == matchStart) { 157 typeswitch (regexp) { 158 case (fastRegexp: FastJSRegExp): { 159 fastRegexp.lastIndex = 160 AdvanceStringIndexFast(string, fastRegexp.lastIndex, unicode); 161 } 162 case (Object): { 163 const lastIndex: JSAny = SlowLoadLastIndex(regexp); 164 const thisIndex: Number = ToLength_Inline(lastIndex); 165 const nextIndex: Number = 166 AdvanceStringIndexSlow(string, thisIndex, unicode); 167 SlowStoreLastIndex(regexp, nextIndex); 168 } 169 } 170 } 171 } 172 173 return result + SubString(string, lastMatchEnd, string.length_smi); 174} 175 176transitioning builtin RegExpReplace(implicit context: Context)( 177 regexp: FastJSRegExp, string: String, replaceValue: JSAny): String { 178 // TODO(pwong): Remove assert when all callers (StringPrototypeReplace) are 179 // from Torque. 180 assert(Is<FastJSRegExp>(regexp)); 181 182 // 2. Is {replace_value} callable? 183 typeswitch (replaceValue) { 184 case (replaceFn: Callable): { 185 return regexp.global ? 186 RegExpReplaceFastGlobalCallable(regexp, string, replaceFn) : 187 StringReplaceNonGlobalRegExpWithFunction(string, regexp, replaceFn); 188 } 189 case (JSAny): { 190 const stableRegexp: JSRegExp = regexp; 191 const replaceString: String = ToString_Inline(replaceValue); 192 193 try { 194 // ToString(replaceValue) could potentially change the shape of the 195 // RegExp object. Recheck that we are still on the fast path and bail 196 // to runtime otherwise. 197 const fastRegexp = Cast<FastJSRegExp>(stableRegexp) otherwise Runtime; 198 if (StringIndexOf( 199 replaceString, SingleCharacterStringConstant('$'), 0) != -1) { 200 goto Runtime; 201 } 202 203 return RegExpReplaceFastString(fastRegexp, string, replaceString); 204 } label Runtime deferred { 205 return RegExpReplaceRT(context, stableRegexp, string, replaceString); 206 } 207 } 208 } 209} 210 211const kRegExpReplaceCalledOnSlowRegExp: constexpr int31 212 generates 'v8::Isolate::kRegExpReplaceCalledOnSlowRegExp'; 213 214transitioning javascript builtin RegExpPrototypeReplace( 215 js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny { 216 const methodName: constexpr string = 'RegExp.prototype.@@replace'; 217 218 // RegExpPrototypeReplace is a bit of a beast - a summary of dispatch logic: 219 // 220 // if (!IsFastRegExp(receiver)) CallRuntime(RegExpReplace) 221 // if (IsCallable(replace)) { 222 // if (IsGlobal(receiver)) { 223 // // Called 'fast-path' but contains several runtime calls. 224 // RegExpReplaceFastGlobalCallable() 225 // } else { 226 // CallRuntime(StringReplaceNonGlobalRegExpWithFunction) 227 // } 228 // } else { 229 // if (replace.contains("$")) { 230 // CallRuntime(RegExpReplace) 231 // } else { 232 // RegExpReplaceFastString() 233 // } 234 // } 235 236 const string: JSAny = arguments[0]; 237 const replaceValue: JSAny = arguments[1]; 238 239 // Let rx be the this value. 240 // If Type(rx) is not Object, throw a TypeError exception. 241 const rx = Cast<JSReceiver>(receiver) 242 otherwise ThrowTypeError( 243 MessageTemplate::kIncompatibleMethodReceiver, methodName); 244 245 // Let S be ? ToString(string). 246 const s = ToString_Inline(string); 247 248 // Fast-path checks: 1. Is the {receiver} an unmodified JSRegExp instance? 249 try { 250 const fastRx: FastJSRegExp = Cast<FastJSRegExp>(rx) otherwise Runtime; 251 return RegExpReplace(fastRx, s, replaceValue); 252 } label Runtime deferred { 253 IncrementUseCounter(context, SmiConstant(kRegExpReplaceCalledOnSlowRegExp)); 254 return RegExpReplaceRT(context, rx, s, replaceValue); 255 } 256} 257} 258