[JSC] Implement String.prototype.concat in JS builtins
[WebKit-https.git] / Source / JavaScriptCore / builtins / StringPrototype.js
1 /*
2  * Copyright (C) 2015 Andy VanWagoner <thetalecrafter@gmail.com>.
3  * Copyright (C) 2016 Yusuke Suzuki <utatane.tea@gmail.com>
4  * Copyright (C) 2016 Apple Inc. All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
16  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
19  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
23  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26  */
27
28 function match(regexp)
29 {
30     "use strict";
31
32     if (this == null)
33         @throwTypeError("String.prototype.match requires that |this| not be null or undefined");
34
35     if (regexp != null) {
36         var matcher = regexp.@matchSymbol;
37         if (matcher != @undefined)
38             return matcher.@call(regexp, this);
39     }
40
41     let thisString = @toString(this);
42     let createdRegExp = @regExpCreate(regexp, @undefined);
43     return createdRegExp.@matchSymbol(thisString);
44 }
45
46 @globalPrivate
47 function repeatSlowPath(string, count)
48 {
49     "use strict";
50
51     // Return an empty string.
52     if (count === 0 || string.length === 0)
53         return "";
54
55     // Return the original string.
56     if (count === 1)
57         return string;
58
59     if (string.length * count > @MAX_STRING_LENGTH)
60         @throwOutOfMemoryError();
61
62     // Bit operation onto |count| is safe because |count| should be within Int32 range,
63     // Repeat log N times to generate the repeated string rope.
64     var result = "";
65     var operand = string;
66     while (true) {
67         if (count & 1)
68             result += operand;
69         count >>= 1;
70         if (!count)
71             return result;
72         operand += operand;
73     }
74 }
75
76 @globalPrivate
77 function repeatCharactersSlowPath(string, count)
78 {
79     "use strict";
80     var repeatCount = (count / string.length) | 0;
81     var remainingCharacters = count - repeatCount * string.length;
82     var result = "";
83     var operand = string;
84     // Bit operation onto |repeatCount| is safe because |repeatCount| should be within Int32 range,
85     // Repeat log N times to generate the repeated string rope.
86     while (true) {
87         if (repeatCount & 1)
88             result += operand;
89         repeatCount >>= 1;
90         if (!repeatCount)
91             break;
92         operand += operand;
93     }
94     if (remainingCharacters)
95         result += @stringSubstrInternal.@call(string, 0, remainingCharacters);
96     return result;
97 }
98
99
100 function repeat(count)
101 {
102     "use strict";
103
104     if (this == null)
105         @throwTypeError("String.prototype.repeat requires that |this| not be null or undefined");
106
107     var string = @toString(this);
108     count = @toInteger(count);
109
110     if (count < 0 || count === @Infinity)
111         @throwRangeError("String.prototype.repeat argument must be greater than or equal to 0 and not be Infinity");
112
113     if (string.length === 1)
114         return @repeatCharacter(string, count);
115
116     return @repeatSlowPath(string, count);
117 }
118
119 function padStart(maxLength/*, fillString*/)
120 {
121     "use strict";
122
123     if (this == null)
124         @throwTypeError("String.prototype.padStart requires that |this| not be null or undefined");
125
126     var string = @toString(this);
127     maxLength = @toLength(maxLength);
128
129     var stringLength = string.length;
130     if (maxLength <= stringLength)
131         return string;
132
133     var filler;
134     var fillString = @argument(1);
135     if (fillString === @undefined)
136         filler = " ";
137     else {
138         filler = @toString(fillString);
139         if (filler === "")
140             return string;
141     }
142
143     if (maxLength > @MAX_STRING_LENGTH)
144         @throwOutOfMemoryError();
145
146     var fillLength = maxLength - stringLength;
147     var truncatedStringFiller;
148
149     if (filler.length === 1)
150         truncatedStringFiller = @repeatCharacter(filler, fillLength);
151     else
152         truncatedStringFiller = @repeatCharactersSlowPath(filler, fillLength);
153     return truncatedStringFiller + string;
154 }
155
156 function padEnd(maxLength/*, fillString*/)
157 {
158     "use strict";
159
160     if (this == null)
161         @throwTypeError("String.prototype.padEnd requires that |this| not be null or undefined");
162
163     var string = @toString(this);
164     maxLength = @toLength(maxLength);
165
166     var stringLength = string.length;
167     if (maxLength <= stringLength)
168         return string;
169
170     var filler;
171     var fillString = @argument(1);
172     if (fillString === @undefined)
173         filler = " ";
174     else {
175         filler = @toString(fillString);
176         if (filler === "")
177             return string;
178     }
179
180     if (maxLength > @MAX_STRING_LENGTH)
181         @throwOutOfMemoryError();
182
183     var fillLength = maxLength - stringLength;
184     var truncatedStringFiller;
185
186     if (filler.length === 1)
187         truncatedStringFiller = @repeatCharacter(filler, fillLength);
188     else
189         truncatedStringFiller = @repeatCharactersSlowPath(filler, fillLength);
190     return string + truncatedStringFiller;
191 }
192
193 @globalPrivate
194 function hasObservableSideEffectsForStringReplace(regexp, replacer) {
195     if (replacer !== @regExpPrototypeSymbolReplace)
196         return true;
197     
198     let regexpExec = @tryGetById(regexp, "exec");
199     if (regexpExec !== @regExpBuiltinExec)
200         return true;
201
202     let regexpGlobal = @tryGetById(regexp, "global");
203     if (regexpGlobal !== @regExpProtoGlobalGetter)
204         return true;
205
206     let regexpUnicode = @tryGetById(regexp, "unicode");
207     if (regexpUnicode !== @regExpProtoUnicodeGetter)
208         return true;
209
210     return !@isRegExpObject(regexp);
211 }
212
213 @intrinsic=StringPrototypeReplaceIntrinsic
214 function replace(search, replace)
215 {
216     "use strict";
217
218     if (this == null)
219         @throwTypeError("String.prototype.replace requires that |this| not be null or undefined");
220
221     if (search != null) {
222         let replacer = search.@replaceSymbol;
223         if (replacer !== @undefined) {
224             if (!@hasObservableSideEffectsForStringReplace(search, replacer))
225                 return @toString(this).@replaceUsingRegExp(search, replace);
226             return replacer.@call(search, this, replace);
227         }
228     }
229
230     let thisString = @toString(this);
231     let searchString = @toString(search);
232     return thisString.@replaceUsingStringSearch(searchString, replace);
233 }
234     
235 @globalPrivate
236 function getDefaultCollator()
237 {
238     return @getDefaultCollator.collator || (@getDefaultCollator.collator = new @Collator());
239 }
240     
241 function localeCompare(that/*, locales, options */)
242 {
243     "use strict";
244
245     // 13.1.1 String.prototype.localeCompare (that [, locales [, options ]]) (ECMA-402 2.0)
246     // http://ecma-international.org/publications/standards/Ecma-402.htm
247
248     // 1. Let O be RequireObjectCoercible(this value).
249     if (this == null)
250         @throwTypeError("String.prototype.localeCompare requires that |this| not be null or undefined");
251
252     // 2. Let S be ToString(O).
253     // 3. ReturnIfAbrupt(S).
254     var thisString = @toString(this);
255
256     // 4. Let That be ToString(that).
257     // 5. ReturnIfAbrupt(That).
258     var thatString = @toString(that);
259
260     // Avoid creating a new collator every time for defaults.
261     var locales = @argument(1);
262     var options = @argument(2);
263     if (locales === @undefined && options === @undefined)
264         return @getDefaultCollator().compare(thisString, thatString);
265
266     // 6. Let collator be Construct(%Collator%, Ā«locales, optionsĀ»).
267     // 7. ReturnIfAbrupt(collator).
268     var collator = new @Collator(locales, options);
269
270     // 8. Return CompareStrings(collator, S, That).
271     return collator.compare(thisString, thatString);
272 }
273
274 function search(regexp)
275 {
276     "use strict";
277
278     if (this == null)
279         @throwTypeError("String.prototype.search requires that |this| not be null or undefined");
280
281     if (regexp != null) {
282         var searcher = regexp.@searchSymbol;
283         if (searcher != @undefined)
284             return searcher.@call(regexp, this);
285     }
286
287     var thisString = @toString(this);
288     var createdRegExp = @regExpCreate(regexp, @undefined);
289     return createdRegExp.@searchSymbol(thisString);
290 }
291
292 function split(separator, limit)
293 {
294     "use strict";
295     
296     if (this == null)
297         @throwTypeError("String.prototype.split requires that |this| not be null or undefined");
298     
299     if (separator != null) {
300         var splitter = separator.@splitSymbol;
301         if (splitter != @undefined)
302             return splitter.@call(separator, this, limit);
303     }
304     
305     return @stringSplitFast.@call(this, separator, limit);
306 }
307
308 @globalPrivate
309 function stringConcatSlowPath()
310 {
311     "use strict";
312
313     var result = @toString(this);
314     for (var i = 0, length = arguments.length; i < length; ++i)
315         result += @toString(arguments[i]);
316     return result;
317 }
318
319 function concat(arg /* ... */)
320 {
321     "use strict";
322
323     if (this == null)
324         @throwTypeError("String.prototype.concat requires that |this| not be null or undefined");
325
326     if (@argumentCount() === 1)
327         return @toString(this) + @toString(arg);
328     return @tailCallForwardArguments(@stringConcatSlowPath, this);
329 }
330
331 @globalPrivate
332 function createHTML(func, string, tag, attribute, value)
333 {
334     "use strict";
335     if (string == null)
336         @throwTypeError(`${func} requires that |this| not be null or undefined`);
337     let S = @toString(string);
338     let p1 = "<" + tag;
339     if (attribute) {
340         let V = @toString(value);
341         let escapedV = V.@replaceUsingRegExp(/"/g, '&quot;');
342         p1 = p1 + " " + @toString(attribute) + '="' + escapedV + '"'
343     }
344     let p2 = p1 + ">"
345     let p3 = p2 + S;
346     let p4 = p3 + "</" + tag + ">";
347     return p4;
348 }
349
350 function anchor(url)
351 {
352     "use strict";
353     return @createHTML("String.prototype.link", this, "a", "name", url)
354 }
355
356 function big()
357 {
358     "use strict";
359     return @createHTML("String.prototype.big", this, "big", "", "");
360 }
361
362 function blink()
363 {
364     "use strict";
365     return @createHTML("String.prototype.blink", this, "blink", "", "");
366 }
367
368 function bold()
369 {
370     "use strict";
371     return @createHTML("String.prototype.bold", this, "b", "", "");
372 }
373
374 function fixed()
375 {
376     "use strict";
377     return @createHTML("String.prototype.fixed", this, "tt", "", "");
378 }
379
380 function fontcolor(color)
381 {
382     "use strict";
383     return @createHTML("String.prototype.fontcolor", this, "font", "color", color);
384 }
385
386 function fontsize(size)
387 {
388     "use strict";
389     return @createHTML("String.prototype.fontsize", this, "font", "size", size);
390 }
391
392 function italics()
393 {
394     "use strict";
395     return @createHTML("String.prototype.italics", this, "i", "", "");
396 }
397
398 function link(url)
399 {
400     "use strict";
401     return @createHTML("String.prototype.link", this, "a", "href", url)
402 }
403
404 function small()
405 {
406     "use strict";
407     return @createHTML("String.prototype.small", this, "small", "", "");
408 }
409
410 function strike()
411 {
412     "use strict";
413     return @createHTML("String.prototype.strike", this, "strike", "", "");
414 }
415
416 function sub()
417 {
418     "use strict";
419     return @createHTML("String.prototype.sub", this, "sub", "", "");
420 }
421
422 function sup()
423 {
424     "use strict";
425     return @createHTML("String.prototype.sup", this, "sup", "", "");
426 }