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): void { 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, 67 replaceFn: Callable): void { 68 for (let i: intptr = 0; i < matchesLength; i++) { 69 const elArray = 70 Cast<JSArray>(matchesElements.objects[i]) otherwise continue; 71 72 // The JSArray is expanded into the function args by Reflect.apply(). 73 // TODO(jgruber): Remove indirection through Call->ReflectApply. 74 const replacementObj: JSAny = Call( 75 context, GetReflectApply(), Undefined, replaceFn, Undefined, elArray); 76 77 // Overwrite the i'th element in the results with the string 78 // we got back from the callback function. 79 matchesElements.objects[i] = ToString_Inline(replacementObj); 80 } 81} 82 83transitioning macro RegExpReplaceFastGlobalCallable(implicit context: Context)( 84 regexp: FastJSRegExp, string: String, replaceFn: Callable): String { 85 regexp.lastIndex = 0; 86 87 const kInitialCapacity: intptr = 16; 88 const kInitialLength: Smi = 0; 89 const result: Null|JSArray = RegExpExecMultiple( 90 regexp, string, GetRegExpLastMatchInfo(), 91 AllocateJSArray( 92 ElementsKind::PACKED_ELEMENTS, GetFastPackedElementsJSArrayMap(), 93 kInitialCapacity, kInitialLength)); 94 95 regexp.lastIndex = 0; 96 97 // If no matches, return the subject string. 98 if (result == Null) return string; 99 100 const matches: JSArray = UnsafeCast<JSArray>(result); 101 const matchesLength: Smi = Cast<Smi>(matches.length) otherwise unreachable; 102 const matchesLengthInt: intptr = Convert<intptr>(matchesLength); 103 const matchesElements: FixedArray = UnsafeCast<FixedArray>(matches.elements); 104 105 // Reload last match info since it might have changed. 106 const nofCaptures: Smi = GetRegExpLastMatchInfo().NumberOfCaptures(); 107 108 // If the number of captures is two then there are no explicit captures in 109 // the regexp, just the implicit capture that captures the whole match. In 110 // this case we can simplify quite a bit and end up with something faster. 111 if (nofCaptures == 2) { 112 RegExpReplaceCallableNoExplicitCaptures( 113 matchesElements, matchesLengthInt, string, replaceFn); 114 } else { 115 RegExpReplaceCallableWithExplicitCaptures( 116 matchesElements, matchesLengthInt, replaceFn); 117 } 118 119 return StringBuilderConcat(matches, matchesLength, string); 120} 121 122transitioning macro RegExpReplaceFastString(implicit context: Context)( 123 regexp: JSRegExp, string: String, replaceString: String): String { 124 // The fast path is reached only if {receiver} is an unmodified JSRegExp 125 // instance, {replace_value} is non-callable, and ToString({replace_value}) 126 // does not contain '$', i.e. we're doing a simple string replacement. 127 let result: String = kEmptyString; 128 let lastMatchEnd: Smi = 0; 129 let unicode: bool = false; 130 const replaceLength: Smi = replaceString.length_smi; 131 const fastRegexp = UnsafeCast<FastJSRegExp>(regexp); 132 const global: bool = fastRegexp.global; 133 134 if (global) { 135 unicode = fastRegexp.unicode; 136 fastRegexp.lastIndex = 0; 137 } 138 139 while (true) { 140 const match: RegExpMatchInfo = 141 RegExpPrototypeExecBodyWithoutResultFast(regexp, string) 142 otherwise break; 143 const matchStart: Smi = match.GetStartOfCapture(0); 144 const matchEnd: Smi = match.GetEndOfCapture(0); 145 146 // TODO(jgruber): We could skip many of the checks that using SubString 147 // here entails. 148 result = result + SubString(string, lastMatchEnd, matchStart); 149 lastMatchEnd = matchEnd; 150 151 if (replaceLength != 0) result = result + replaceString; 152 153 // Non-global case ends here after the first replacement. 154 if (!global) break; 155 156 // If match is the empty string, we have to increment lastIndex. 157 if (matchEnd == matchStart) { 158 typeswitch (regexp) { 159 case (fastRegexp: FastJSRegExp): { 160 fastRegexp.lastIndex = 161 AdvanceStringIndexFast(string, fastRegexp.lastIndex, unicode); 162 } 163 case (Object): { 164 const lastIndex: JSAny = SlowLoadLastIndex(regexp); 165 const thisIndex: Number = ToLength_Inline(lastIndex); 166 const nextIndex: Number = 167 AdvanceStringIndexSlow(string, thisIndex, unicode); 168 SlowStoreLastIndex(regexp, nextIndex); 169 } 170 } 171 } 172 } 173 174 return result + SubString(string, lastMatchEnd, string.length_smi); 175} 176 177transitioning builtin RegExpReplace(implicit context: Context)( 178 regexp: FastJSRegExp, string: String, replaceValue: JSAny): String { 179 // TODO(pwong): Remove dcheck when all callers (StringPrototypeReplace) are 180 // from Torque. 181 dcheck(Is<FastJSRegExp>(regexp)); 182 183 // 2. Is {replace_value} callable? 184 typeswitch (replaceValue) { 185 case (replaceFn: Callable): { 186 return regexp.global ? 187 RegExpReplaceFastGlobalCallable(regexp, string, replaceFn) : 188 StringReplaceNonGlobalRegExpWithFunction(string, regexp, replaceFn); 189 } 190 case (JSAny): { 191 const stableRegexp: JSRegExp = regexp; 192 const replaceString: String = ToString_Inline(replaceValue); 193 194 try { 195 // ToString(replaceValue) could potentially change the shape of the 196 // RegExp object. Recheck that we are still on the fast path and bail 197 // to runtime otherwise. 198 const fastRegexp = Cast<FastJSRegExp>(stableRegexp) otherwise Runtime; 199 if (StringIndexOf( 200 replaceString, SingleCharacterStringConstant('$'), 0) != -1) { 201 goto Runtime; 202 } 203 204 return RegExpReplaceFastString(fastRegexp, string, replaceString); 205 } label Runtime deferred { 206 return RegExpReplaceRT(context, stableRegexp, string, replaceString); 207 } 208 } 209 } 210} 211 212const kRegExpReplaceCalledOnSlowRegExp: constexpr int31 213 generates 'v8::Isolate::kRegExpReplaceCalledOnSlowRegExp'; 214 215transitioning javascript builtin RegExpPrototypeReplace( 216 js-implicit context: NativeContext, receiver: JSAny)(...arguments): JSAny { 217 const methodName: constexpr string = 'RegExp.prototype.@@replace'; 218 219 // RegExpPrototypeReplace is a bit of a beast - a summary of dispatch logic: 220 // 221 // if (!IsFastRegExp(receiver)) CallRuntime(RegExpReplace) 222 // if (IsCallable(replace)) { 223 // if (IsGlobal(receiver)) { 224 // // Called 'fast-path' but contains several runtime calls. 225 // RegExpReplaceFastGlobalCallable() 226 // } else { 227 // CallRuntime(StringReplaceNonGlobalRegExpWithFunction) 228 // } 229 // } else { 230 // if (replace.contains("$")) { 231 // CallRuntime(RegExpReplace) 232 // } else { 233 // RegExpReplaceFastString() 234 // } 235 // } 236 237 const string: JSAny = arguments[0]; 238 const replaceValue: JSAny = arguments[1]; 239 240 // Let rx be the this value. 241 // If Type(rx) is not Object, throw a TypeError exception. 242 const rx = Cast<JSReceiver>(receiver) 243 otherwise ThrowTypeError( 244 MessageTemplate::kIncompatibleMethodReceiver, methodName); 245 246 // Let S be ? ToString(string). 247 const s = ToString_Inline(string); 248 249 // Fast-path checks: 1. Is the {receiver} an unmodified JSRegExp instance? 250 try { 251 const fastRx: FastJSRegExp = Cast<FastJSRegExp>(rx) otherwise Runtime; 252 return RegExpReplace(fastRx, s, replaceValue); 253 } label Runtime deferred { 254 IncrementUseCounter(context, SmiConstant(kRegExpReplaceCalledOnSlowRegExp)); 255 return RegExpReplaceRT(context, rx, s, replaceValue); 256 } 257} 258} 259