387e5bef51febf340ae6e117a94c6621180f5243
[platform/upstream/v8.git] / src / string.js
1 // Copyright 2012 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 (function(global, utils) {
6
7 %CheckIsBootstrapping();
8
9 // -------------------------------------------------------------------
10 // Imports
11
12 var GlobalRegExp = global.RegExp;
13 var GlobalString = global.String;
14 var InternalArray = utils.InternalArray;
15 var InternalPackedArray = utils.InternalPackedArray;
16
17 var ArrayIndexOf;
18 var ArrayJoin;
19 var MathMax;
20 var MathMin;
21 var RegExpExec;
22 var RegExpExecNoTests;
23 var RegExpLastMatchInfo;
24
25 utils.Import(function(from) {
26   ArrayIndexOf = from.ArrayIndexOf;
27   ArrayJoin = from.ArrayJoin;
28   MathMax = from.MathMax;
29   MathMin = from.MathMin;
30   RegExpExec = from.RegExpExec;
31   RegExpExecNoTests = from.RegExpExecNoTests;
32   RegExpLastMatchInfo = from.RegExpLastMatchInfo;
33 });
34
35 //-------------------------------------------------------------------
36
37 function StringConstructor(x) {
38   if (%_ArgumentsLength() == 0) x = '';
39   if (%_IsConstructCall()) {
40     %_SetValueOf(this, TO_STRING_INLINE(x));
41   } else {
42     return IS_SYMBOL(x) ?
43         %_CallFunction(x, $symbolToString) : TO_STRING_INLINE(x);
44   }
45 }
46
47
48 // ECMA-262 section 15.5.4.2
49 function StringToString() {
50   if (!IS_STRING(this) && !IS_STRING_WRAPPER(this)) {
51     throw MakeTypeError(kNotGeneric, 'String.prototype.toString');
52   }
53   return %_ValueOf(this);
54 }
55
56
57 // ECMA-262 section 15.5.4.3
58 function StringValueOf() {
59   if (!IS_STRING(this) && !IS_STRING_WRAPPER(this)) {
60     throw MakeTypeError(kNotGeneric, 'String.prototype.valueOf');
61   }
62   return %_ValueOf(this);
63 }
64
65
66 // ECMA-262, section 15.5.4.4
67 function StringCharAtJS(pos) {
68   CHECK_OBJECT_COERCIBLE(this, "String.prototype.charAt");
69
70   var result = %_StringCharAt(this, pos);
71   if (%_IsSmi(result)) {
72     result = %_StringCharAt(TO_STRING_INLINE(this), TO_INTEGER(pos));
73   }
74   return result;
75 }
76
77
78 // ECMA-262 section 15.5.4.5
79 function StringCharCodeAtJS(pos) {
80   CHECK_OBJECT_COERCIBLE(this, "String.prototype.charCodeAt");
81
82   var result = %_StringCharCodeAt(this, pos);
83   if (!%_IsSmi(result)) {
84     result = %_StringCharCodeAt(TO_STRING_INLINE(this), TO_INTEGER(pos));
85   }
86   return result;
87 }
88
89
90 // ECMA-262, section 15.5.4.6
91 function StringConcat(other /* and more */) {  // length == 1
92   CHECK_OBJECT_COERCIBLE(this, "String.prototype.concat");
93   var len = %_ArgumentsLength();
94   var this_as_string = TO_STRING_INLINE(this);
95   if (len === 1) {
96     return this_as_string + TO_STRING_INLINE(other);
97   }
98   var parts = new InternalArray(len + 1);
99   parts[0] = this_as_string;
100   for (var i = 0; i < len; i++) {
101     var part = %_Arguments(i);
102     parts[i + 1] = TO_STRING_INLINE(part);
103   }
104   return %StringBuilderConcat(parts, len + 1, "");
105 }
106
107
108 // ECMA-262 section 15.5.4.7
109 function StringIndexOfJS(pattern /* position */) {  // length == 1
110   CHECK_OBJECT_COERCIBLE(this, "String.prototype.indexOf");
111
112   var subject = TO_STRING_INLINE(this);
113   pattern = TO_STRING_INLINE(pattern);
114   var index = 0;
115   if (%_ArgumentsLength() > 1) {
116     index = %_Arguments(1);  // position
117     index = TO_INTEGER(index);
118     if (index < 0) index = 0;
119     if (index > subject.length) index = subject.length;
120   }
121   return %StringIndexOf(subject, pattern, index);
122 }
123
124
125 // ECMA-262 section 15.5.4.8
126 function StringLastIndexOfJS(pat /* position */) {  // length == 1
127   CHECK_OBJECT_COERCIBLE(this, "String.prototype.lastIndexOf");
128
129   var sub = TO_STRING_INLINE(this);
130   var subLength = sub.length;
131   var pat = TO_STRING_INLINE(pat);
132   var patLength = pat.length;
133   var index = subLength - patLength;
134   if (%_ArgumentsLength() > 1) {
135     var position = $toNumber(%_Arguments(1));
136     if (!NUMBER_IS_NAN(position)) {
137       position = TO_INTEGER(position);
138       if (position < 0) {
139         position = 0;
140       }
141       if (position + patLength < subLength) {
142         index = position;
143       }
144     }
145   }
146   if (index < 0) {
147     return -1;
148   }
149   return %StringLastIndexOf(sub, pat, index);
150 }
151
152
153 // ECMA-262 section 15.5.4.9
154 //
155 // This function is implementation specific.  For now, we do not
156 // do anything locale specific.
157 function StringLocaleCompareJS(other) {
158   CHECK_OBJECT_COERCIBLE(this, "String.prototype.localeCompare");
159
160   return %StringLocaleCompare(TO_STRING_INLINE(this),
161                               TO_STRING_INLINE(other));
162 }
163
164
165 // ECMA-262 section 15.5.4.10
166 function StringMatchJS(regexp) {
167   CHECK_OBJECT_COERCIBLE(this, "String.prototype.match");
168
169   var subject = TO_STRING_INLINE(this);
170   if (IS_REGEXP(regexp)) {
171     // Emulate RegExp.prototype.exec's side effect in step 5, even though
172     // value is discarded.
173     var lastIndex = regexp.lastIndex;
174     TO_INTEGER_FOR_SIDE_EFFECT(lastIndex);
175     if (!regexp.global) return RegExpExecNoTests(regexp, subject, 0);
176     var result = %StringMatch(subject, regexp, RegExpLastMatchInfo);
177     if (result !== null) $regexpLastMatchInfoOverride = null;
178     regexp.lastIndex = 0;
179     return result;
180   }
181   // Non-regexp argument.
182   regexp = new GlobalRegExp(regexp);
183   return RegExpExecNoTests(regexp, subject, 0);
184 }
185
186
187 // ECMA-262 v6, section 21.1.3.12
188 //
189 // For now we do nothing, as proper normalization requires big tables.
190 // If Intl is enabled, then i18n.js will override it and provide the the
191 // proper functionality.
192 function StringNormalizeJS(form) {
193   CHECK_OBJECT_COERCIBLE(this, "String.prototype.normalize");
194
195   var form = IS_UNDEFINED(form) ? 'NFC' : TO_STRING_INLINE(form);
196
197   var NORMALIZATION_FORMS = ['NFC', 'NFD', 'NFKC', 'NFKD'];
198   var normalizationForm =
199       %_CallFunction(NORMALIZATION_FORMS, form, ArrayIndexOf);
200   if (normalizationForm === -1) {
201     throw MakeRangeError(kNormalizationForm,
202                          %_CallFunction(NORMALIZATION_FORMS, ', ', ArrayJoin));
203   }
204
205   return %_ValueOf(this);
206 }
207
208
209 // This has the same size as the RegExpLastMatchInfo array, and can be used
210 // for functions that expect that structure to be returned.  It is used when
211 // the needle is a string rather than a regexp.  In this case we can't update
212 // lastMatchArray without erroneously affecting the properties on the global
213 // RegExp object.
214 var reusableMatchInfo = [2, "", "", -1, -1];
215
216
217 // ECMA-262, section 15.5.4.11
218 function StringReplace(search, replace) {
219   CHECK_OBJECT_COERCIBLE(this, "String.prototype.replace");
220
221   var subject = TO_STRING_INLINE(this);
222
223   // Decision tree for dispatch
224   // .. regexp search
225   // .... string replace
226   // ...... non-global search
227   // ........ empty string replace
228   // ........ non-empty string replace (with $-expansion)
229   // ...... global search
230   // ........ no need to circumvent last match info override
231   // ........ need to circument last match info override
232   // .... function replace
233   // ...... global search
234   // ...... non-global search
235   // .. string search
236   // .... special case that replaces with one single character
237   // ...... function replace
238   // ...... string replace (with $-expansion)
239
240   if (IS_REGEXP(search)) {
241     // Emulate RegExp.prototype.exec's side effect in step 5, even if
242     // value is discarded.
243     var lastIndex = search.lastIndex;
244     TO_INTEGER_FOR_SIDE_EFFECT(lastIndex);
245
246     if (!IS_SPEC_FUNCTION(replace)) {
247       replace = TO_STRING_INLINE(replace);
248
249       if (!search.global) {
250         // Non-global regexp search, string replace.
251         var match = RegExpExec(search, subject, 0);
252         if (match == null) {
253           search.lastIndex = 0
254           return subject;
255         }
256         if (replace.length == 0) {
257           return %_SubString(subject, 0, match[CAPTURE0]) +
258                  %_SubString(subject, match[CAPTURE1], subject.length)
259         }
260         return ExpandReplacement(replace, subject, RegExpLastMatchInfo,
261                                  %_SubString(subject, 0, match[CAPTURE0])) +
262                %_SubString(subject, match[CAPTURE1], subject.length);
263       }
264
265       // Global regexp search, string replace.
266       search.lastIndex = 0;
267       if ($regexpLastMatchInfoOverride == null) {
268         return %StringReplaceGlobalRegExpWithString(
269             subject, search, replace, RegExpLastMatchInfo);
270       } else {
271         // We use this hack to detect whether StringReplaceRegExpWithString
272         // found at least one hit. In that case we need to remove any
273         // override.
274         var saved_subject = RegExpLastMatchInfo[LAST_SUBJECT_INDEX];
275         RegExpLastMatchInfo[LAST_SUBJECT_INDEX] = 0;
276         var answer = %StringReplaceGlobalRegExpWithString(
277             subject, search, replace, RegExpLastMatchInfo);
278         if (%_IsSmi(RegExpLastMatchInfo[LAST_SUBJECT_INDEX])) {
279           RegExpLastMatchInfo[LAST_SUBJECT_INDEX] = saved_subject;
280         } else {
281           $regexpLastMatchInfoOverride = null;
282         }
283         return answer;
284       }
285     }
286
287     if (search.global) {
288       // Global regexp search, function replace.
289       return StringReplaceGlobalRegExpWithFunction(subject, search, replace);
290     }
291     // Non-global regexp search, function replace.
292     return StringReplaceNonGlobalRegExpWithFunction(subject, search, replace);
293   }
294
295   search = TO_STRING_INLINE(search);
296
297   if (search.length == 1 &&
298       subject.length > 0xFF &&
299       IS_STRING(replace) &&
300       %StringIndexOf(replace, '$', 0) < 0) {
301     // Searching by traversing a cons string tree and replace with cons of
302     // slices works only when the replaced string is a single character, being
303     // replaced by a simple string and only pays off for long strings.
304     return %StringReplaceOneCharWithString(subject, search, replace);
305   }
306   var start = %StringIndexOf(subject, search, 0);
307   if (start < 0) return subject;
308   var end = start + search.length;
309
310   var result = %_SubString(subject, 0, start);
311
312   // Compute the string to replace with.
313   if (IS_SPEC_FUNCTION(replace)) {
314     var receiver = UNDEFINED;
315     result += %_CallFunction(receiver, search, start, subject, replace);
316   } else {
317     reusableMatchInfo[CAPTURE0] = start;
318     reusableMatchInfo[CAPTURE1] = end;
319     result = ExpandReplacement(TO_STRING_INLINE(replace),
320                                subject,
321                                reusableMatchInfo,
322                                result);
323   }
324
325   return result + %_SubString(subject, end, subject.length);
326 }
327
328
329 // Expand the $-expressions in the string and return a new string with
330 // the result.
331 function ExpandReplacement(string, subject, matchInfo, result) {
332   var length = string.length;
333   var next = %StringIndexOf(string, '$', 0);
334   if (next < 0) {
335     if (length > 0) result += string;
336     return result;
337   }
338
339   if (next > 0) result += %_SubString(string, 0, next);
340
341   while (true) {
342     var expansion = '$';
343     var position = next + 1;
344     if (position < length) {
345       var peek = %_StringCharCodeAt(string, position);
346       if (peek == 36) {         // $$
347         ++position;
348         result += '$';
349       } else if (peek == 38) {  // $& - match
350         ++position;
351         result +=
352           %_SubString(subject, matchInfo[CAPTURE0], matchInfo[CAPTURE1]);
353       } else if (peek == 96) {  // $` - prefix
354         ++position;
355         result += %_SubString(subject, 0, matchInfo[CAPTURE0]);
356       } else if (peek == 39) {  // $' - suffix
357         ++position;
358         result += %_SubString(subject, matchInfo[CAPTURE1], subject.length);
359       } else if (peek >= 48 && peek <= 57) {
360         // Valid indices are $1 .. $9, $01 .. $09 and $10 .. $99
361         var scaled_index = (peek - 48) << 1;
362         var advance = 1;
363         var number_of_captures = NUMBER_OF_CAPTURES(matchInfo);
364         if (position + 1 < string.length) {
365           var next = %_StringCharCodeAt(string, position + 1);
366           if (next >= 48 && next <= 57) {
367             var new_scaled_index = scaled_index * 10 + ((next - 48) << 1);
368             if (new_scaled_index < number_of_captures) {
369               scaled_index = new_scaled_index;
370               advance = 2;
371             }
372           }
373         }
374         if (scaled_index != 0 && scaled_index < number_of_captures) {
375           var start = matchInfo[CAPTURE(scaled_index)];
376           if (start >= 0) {
377             result +=
378               %_SubString(subject, start, matchInfo[CAPTURE(scaled_index + 1)]);
379           }
380           position += advance;
381         } else {
382           result += '$';
383         }
384       } else {
385         result += '$';
386       }
387     } else {
388       result += '$';
389     }
390
391     // Go the the next $ in the string.
392     next = %StringIndexOf(string, '$', position);
393
394     // Return if there are no more $ characters in the string. If we
395     // haven't reached the end, we need to append the suffix.
396     if (next < 0) {
397       if (position < length) {
398         result += %_SubString(string, position, length);
399       }
400       return result;
401     }
402
403     // Append substring between the previous and the next $ character.
404     if (next > position) {
405       result += %_SubString(string, position, next);
406     }
407   }
408   return result;
409 }
410
411
412 // Compute the string of a given regular expression capture.
413 function CaptureString(string, lastCaptureInfo, index) {
414   // Scale the index.
415   var scaled = index << 1;
416   // Compute start and end.
417   var start = lastCaptureInfo[CAPTURE(scaled)];
418   // If start isn't valid, return undefined.
419   if (start < 0) return;
420   var end = lastCaptureInfo[CAPTURE(scaled + 1)];
421   return %_SubString(string, start, end);
422 }
423
424
425 // TODO(lrn): This array will survive indefinitely if replace is never
426 // called again. However, it will be empty, since the contents are cleared
427 // in the finally block.
428 var reusableReplaceArray = new InternalArray(4);
429
430 // Helper function for replacing regular expressions with the result of a
431 // function application in String.prototype.replace.
432 function StringReplaceGlobalRegExpWithFunction(subject, regexp, replace) {
433   var resultArray = reusableReplaceArray;
434   if (resultArray) {
435     reusableReplaceArray = null;
436   } else {
437     // Inside a nested replace (replace called from the replacement function
438     // of another replace) or we have failed to set the reusable array
439     // back due to an exception in a replacement function. Create a new
440     // array to use in the future, or until the original is written back.
441     resultArray = new InternalArray(16);
442   }
443   var res = %RegExpExecMultiple(regexp,
444                                 subject,
445                                 RegExpLastMatchInfo,
446                                 resultArray);
447   regexp.lastIndex = 0;
448   if (IS_NULL(res)) {
449     // No matches at all.
450     reusableReplaceArray = resultArray;
451     return subject;
452   }
453   var len = res.length;
454   if (NUMBER_OF_CAPTURES(RegExpLastMatchInfo) == 2) {
455     // If the number of captures is two then there are no explicit captures in
456     // the regexp, just the implicit capture that captures the whole match.  In
457     // this case we can simplify quite a bit and end up with something faster.
458     // The builder will consist of some integers that indicate slices of the
459     // input string and some replacements that were returned from the replace
460     // function.
461     var match_start = 0;
462     var override = new InternalPackedArray(null, 0, subject);
463     for (var i = 0; i < len; i++) {
464       var elem = res[i];
465       if (%_IsSmi(elem)) {
466         // Integers represent slices of the original string.  Use these to
467         // get the offsets we need for the override array (so things like
468         // RegExp.leftContext work during the callback function.
469         if (elem > 0) {
470           match_start = (elem >> 11) + (elem & 0x7ff);
471         } else {
472           match_start = res[++i] - elem;
473         }
474       } else {
475         override[0] = elem;
476         override[1] = match_start;
477         $regexpLastMatchInfoOverride = override;
478         var func_result =
479             %_CallFunction(UNDEFINED, elem, match_start, subject, replace);
480         // Overwrite the i'th element in the results with the string we got
481         // back from the callback function.
482         res[i] = TO_STRING_INLINE(func_result);
483         match_start += elem.length;
484       }
485     }
486   } else {
487     for (var i = 0; i < len; i++) {
488       var elem = res[i];
489       if (!%_IsSmi(elem)) {
490         // elem must be an Array.
491         // Use the apply argument as backing for global RegExp properties.
492         $regexpLastMatchInfoOverride = elem;
493         var func_result = %Apply(replace, UNDEFINED, elem, 0, elem.length);
494         // Overwrite the i'th element in the results with the string we got
495         // back from the callback function.
496         res[i] = TO_STRING_INLINE(func_result);
497       }
498     }
499   }
500   var result = %StringBuilderConcat(res, res.length, subject);
501   resultArray.length = 0;
502   reusableReplaceArray = resultArray;
503   return result;
504 }
505
506
507 function StringReplaceNonGlobalRegExpWithFunction(subject, regexp, replace) {
508   var matchInfo = RegExpExec(regexp, subject, 0);
509   if (IS_NULL(matchInfo)) {
510     regexp.lastIndex = 0;
511     return subject;
512   }
513   var index = matchInfo[CAPTURE0];
514   var result = %_SubString(subject, 0, index);
515   var endOfMatch = matchInfo[CAPTURE1];
516   // Compute the parameter list consisting of the match, captures, index,
517   // and subject for the replace function invocation.
518   // The number of captures plus one for the match.
519   var m = NUMBER_OF_CAPTURES(matchInfo) >> 1;
520   var replacement;
521   if (m == 1) {
522     // No captures, only the match, which is always valid.
523     var s = %_SubString(subject, index, endOfMatch);
524     // Don't call directly to avoid exposing the built-in global object.
525     replacement = %_CallFunction(UNDEFINED, s, index, subject, replace);
526   } else {
527     var parameters = new InternalArray(m + 2);
528     for (var j = 0; j < m; j++) {
529       parameters[j] = CaptureString(subject, matchInfo, j);
530     }
531     parameters[j] = index;
532     parameters[j + 1] = subject;
533
534     replacement = %Apply(replace, UNDEFINED, parameters, 0, j + 2);
535   }
536
537   result += replacement;  // The add method converts to string if necessary.
538   // Can't use matchInfo any more from here, since the function could
539   // overwrite it.
540   return result + %_SubString(subject, endOfMatch, subject.length);
541 }
542
543
544 // ECMA-262 section 15.5.4.12
545 function StringSearch(re) {
546   CHECK_OBJECT_COERCIBLE(this, "String.prototype.search");
547
548   var regexp;
549   if (IS_STRING(re)) {
550     regexp = %_GetFromCache(STRING_TO_REGEXP_CACHE_ID, re);
551   } else if (IS_REGEXP(re)) {
552     regexp = re;
553   } else {
554     regexp = new GlobalRegExp(re);
555   }
556   var match = RegExpExec(regexp, TO_STRING_INLINE(this), 0);
557   if (match) {
558     return match[CAPTURE0];
559   }
560   return -1;
561 }
562
563
564 // ECMA-262 section 15.5.4.13
565 function StringSlice(start, end) {
566   CHECK_OBJECT_COERCIBLE(this, "String.prototype.slice");
567
568   var s = TO_STRING_INLINE(this);
569   var s_len = s.length;
570   var start_i = TO_INTEGER(start);
571   var end_i = s_len;
572   if (!IS_UNDEFINED(end)) {
573     end_i = TO_INTEGER(end);
574   }
575
576   if (start_i < 0) {
577     start_i += s_len;
578     if (start_i < 0) {
579       start_i = 0;
580     }
581   } else {
582     if (start_i > s_len) {
583       return '';
584     }
585   }
586
587   if (end_i < 0) {
588     end_i += s_len;
589     if (end_i < 0) {
590       return '';
591     }
592   } else {
593     if (end_i > s_len) {
594       end_i = s_len;
595     }
596   }
597
598   if (end_i <= start_i) {
599     return '';
600   }
601
602   return %_SubString(s, start_i, end_i);
603 }
604
605
606 // ECMA-262 section 15.5.4.14
607 function StringSplitJS(separator, limit) {
608   CHECK_OBJECT_COERCIBLE(this, "String.prototype.split");
609
610   var subject = TO_STRING_INLINE(this);
611   limit = (IS_UNDEFINED(limit)) ? 0xffffffff : TO_UINT32(limit);
612
613   var length = subject.length;
614   if (!IS_REGEXP(separator)) {
615     var separator_string = TO_STRING_INLINE(separator);
616
617     if (limit === 0) return [];
618
619     // ECMA-262 says that if separator is undefined, the result should
620     // be an array of size 1 containing the entire string.
621     if (IS_UNDEFINED(separator)) return [subject];
622
623     var separator_length = separator_string.length;
624
625     // If the separator string is empty then return the elements in the subject.
626     if (separator_length === 0) return %StringToArray(subject, limit);
627
628     var result = %StringSplit(subject, separator_string, limit);
629
630     return result;
631   }
632
633   if (limit === 0) return [];
634
635   // Separator is a regular expression.
636   return StringSplitOnRegExp(subject, separator, limit, length);
637 }
638
639
640 function StringSplitOnRegExp(subject, separator, limit, length) {
641   if (length === 0) {
642     if (RegExpExec(separator, subject, 0, 0) != null) {
643       return [];
644     }
645     return [subject];
646   }
647
648   var currentIndex = 0;
649   var startIndex = 0;
650   var startMatch = 0;
651   var result = new InternalArray();
652
653   outer_loop:
654   while (true) {
655
656     if (startIndex === length) {
657       result[result.length] = %_SubString(subject, currentIndex, length);
658       break;
659     }
660
661     var matchInfo = RegExpExec(separator, subject, startIndex);
662     if (matchInfo == null || length === (startMatch = matchInfo[CAPTURE0])) {
663       result[result.length] = %_SubString(subject, currentIndex, length);
664       break;
665     }
666     var endIndex = matchInfo[CAPTURE1];
667
668     // We ignore a zero-length match at the currentIndex.
669     if (startIndex === endIndex && endIndex === currentIndex) {
670       startIndex++;
671       continue;
672     }
673
674     result[result.length] = %_SubString(subject, currentIndex, startMatch);
675
676     if (result.length === limit) break;
677
678     var matchinfo_len = NUMBER_OF_CAPTURES(matchInfo) + REGEXP_FIRST_CAPTURE;
679     for (var i = REGEXP_FIRST_CAPTURE + 2; i < matchinfo_len; ) {
680       var start = matchInfo[i++];
681       var end = matchInfo[i++];
682       if (end != -1) {
683         result[result.length] = %_SubString(subject, start, end);
684       } else {
685         result[result.length] = UNDEFINED;
686       }
687       if (result.length === limit) break outer_loop;
688     }
689
690     startIndex = currentIndex = endIndex;
691   }
692   var array_result = [];
693   %MoveArrayContents(result, array_result);
694   return array_result;
695 }
696
697
698 // ECMA-262 section 15.5.4.15
699 function StringSubstring(start, end) {
700   CHECK_OBJECT_COERCIBLE(this, "String.prototype.subString");
701
702   var s = TO_STRING_INLINE(this);
703   var s_len = s.length;
704
705   var start_i = TO_INTEGER(start);
706   if (start_i < 0) {
707     start_i = 0;
708   } else if (start_i > s_len) {
709     start_i = s_len;
710   }
711
712   var end_i = s_len;
713   if (!IS_UNDEFINED(end)) {
714     end_i = TO_INTEGER(end);
715     if (end_i > s_len) {
716       end_i = s_len;
717     } else {
718       if (end_i < 0) end_i = 0;
719       if (start_i > end_i) {
720         var tmp = end_i;
721         end_i = start_i;
722         start_i = tmp;
723       }
724     }
725   }
726
727   return %_SubString(s, start_i, end_i);
728 }
729
730
731 // ES6 draft, revision 26 (2014-07-18), section B.2.3.1
732 function StringSubstr(start, n) {
733   CHECK_OBJECT_COERCIBLE(this, "String.prototype.substr");
734
735   var s = TO_STRING_INLINE(this);
736   var len;
737
738   // Correct n: If not given, set to string length; if explicitly
739   // set to undefined, zero, or negative, returns empty string.
740   if (IS_UNDEFINED(n)) {
741     len = s.length;
742   } else {
743     len = TO_INTEGER(n);
744     if (len <= 0) return '';
745   }
746
747   // Correct start: If not given (or undefined), set to zero; otherwise
748   // convert to integer and handle negative case.
749   if (IS_UNDEFINED(start)) {
750     start = 0;
751   } else {
752     start = TO_INTEGER(start);
753     // If positive, and greater than or equal to the string length,
754     // return empty string.
755     if (start >= s.length) return '';
756     // If negative and absolute value is larger than the string length,
757     // use zero.
758     if (start < 0) {
759       start += s.length;
760       if (start < 0) start = 0;
761     }
762   }
763
764   var end = start + len;
765   if (end > s.length) end = s.length;
766
767   return %_SubString(s, start, end);
768 }
769
770
771 // ECMA-262, 15.5.4.16
772 function StringToLowerCaseJS() {
773   CHECK_OBJECT_COERCIBLE(this, "String.prototype.toLowerCase");
774
775   return %StringToLowerCase(TO_STRING_INLINE(this));
776 }
777
778
779 // ECMA-262, 15.5.4.17
780 function StringToLocaleLowerCase() {
781   CHECK_OBJECT_COERCIBLE(this, "String.prototype.toLocaleLowerCase");
782
783   return %StringToLowerCase(TO_STRING_INLINE(this));
784 }
785
786
787 // ECMA-262, 15.5.4.18
788 function StringToUpperCaseJS() {
789   CHECK_OBJECT_COERCIBLE(this, "String.prototype.toUpperCase");
790
791   return %StringToUpperCase(TO_STRING_INLINE(this));
792 }
793
794
795 // ECMA-262, 15.5.4.19
796 function StringToLocaleUpperCase() {
797   CHECK_OBJECT_COERCIBLE(this, "String.prototype.toLocaleUpperCase");
798
799   return %StringToUpperCase(TO_STRING_INLINE(this));
800 }
801
802 // ES5, 15.5.4.20
803 function StringTrimJS() {
804   CHECK_OBJECT_COERCIBLE(this, "String.prototype.trim");
805
806   return %StringTrim(TO_STRING_INLINE(this), true, true);
807 }
808
809 function StringTrimLeft() {
810   CHECK_OBJECT_COERCIBLE(this, "String.prototype.trimLeft");
811
812   return %StringTrim(TO_STRING_INLINE(this), true, false);
813 }
814
815 function StringTrimRight() {
816   CHECK_OBJECT_COERCIBLE(this, "String.prototype.trimRight");
817
818   return %StringTrim(TO_STRING_INLINE(this), false, true);
819 }
820
821
822 // ECMA-262, section 15.5.3.2
823 function StringFromCharCode(code) {
824   var n = %_ArgumentsLength();
825   if (n == 1) {
826     if (!%_IsSmi(code)) code = $toNumber(code);
827     return %_StringCharFromCode(code & 0xffff);
828   }
829
830   var one_byte = %NewString(n, NEW_ONE_BYTE_STRING);
831   var i;
832   for (i = 0; i < n; i++) {
833     var code = %_Arguments(i);
834     if (!%_IsSmi(code)) code = $toNumber(code) & 0xffff;
835     if (code < 0) code = code & 0xffff;
836     if (code > 0xff) break;
837     %_OneByteSeqStringSetChar(i, code, one_byte);
838   }
839   if (i == n) return one_byte;
840   one_byte = %TruncateString(one_byte, i);
841
842   var two_byte = %NewString(n - i, NEW_TWO_BYTE_STRING);
843   for (var j = 0; i < n; i++, j++) {
844     var code = %_Arguments(i);
845     if (!%_IsSmi(code)) code = $toNumber(code) & 0xffff;
846     %_TwoByteSeqStringSetChar(j, code, two_byte);
847   }
848   return one_byte + two_byte;
849 }
850
851
852 // ES6 draft, revision 26 (2014-07-18), section B.2.3.2.1
853 function HtmlEscape(str) {
854   return %_CallFunction(TO_STRING_INLINE(str), /"/g, "&quot;", StringReplace);
855 }
856
857
858 // ES6 draft, revision 26 (2014-07-18), section B.2.3.2
859 function StringAnchor(name) {
860   CHECK_OBJECT_COERCIBLE(this, "String.prototype.anchor");
861   return "<a name=\"" + HtmlEscape(name) + "\">" + TO_STRING_INLINE(this) +
862          "</a>";
863 }
864
865
866 // ES6 draft, revision 26 (2014-07-18), section B.2.3.3
867 function StringBig() {
868   CHECK_OBJECT_COERCIBLE(this, "String.prototype.big");
869   return "<big>" + TO_STRING_INLINE(this) + "</big>";
870 }
871
872
873 // ES6 draft, revision 26 (2014-07-18), section B.2.3.4
874 function StringBlink() {
875   CHECK_OBJECT_COERCIBLE(this, "String.prototype.blink");
876   return "<blink>" + TO_STRING_INLINE(this) + "</blink>";
877 }
878
879
880 // ES6 draft, revision 26 (2014-07-18), section B.2.3.5
881 function StringBold() {
882   CHECK_OBJECT_COERCIBLE(this, "String.prototype.bold");
883   return "<b>" + TO_STRING_INLINE(this) + "</b>";
884 }
885
886
887 // ES6 draft, revision 26 (2014-07-18), section B.2.3.6
888 function StringFixed() {
889   CHECK_OBJECT_COERCIBLE(this, "String.prototype.fixed");
890   return "<tt>" + TO_STRING_INLINE(this) + "</tt>";
891 }
892
893
894 // ES6 draft, revision 26 (2014-07-18), section B.2.3.7
895 function StringFontcolor(color) {
896   CHECK_OBJECT_COERCIBLE(this, "String.prototype.fontcolor");
897   return "<font color=\"" + HtmlEscape(color) + "\">" + TO_STRING_INLINE(this) +
898          "</font>";
899 }
900
901
902 // ES6 draft, revision 26 (2014-07-18), section B.2.3.8
903 function StringFontsize(size) {
904   CHECK_OBJECT_COERCIBLE(this, "String.prototype.fontsize");
905   return "<font size=\"" + HtmlEscape(size) + "\">" + TO_STRING_INLINE(this) +
906          "</font>";
907 }
908
909
910 // ES6 draft, revision 26 (2014-07-18), section B.2.3.9
911 function StringItalics() {
912   CHECK_OBJECT_COERCIBLE(this, "String.prototype.italics");
913   return "<i>" + TO_STRING_INLINE(this) + "</i>";
914 }
915
916
917 // ES6 draft, revision 26 (2014-07-18), section B.2.3.10
918 function StringLink(s) {
919   CHECK_OBJECT_COERCIBLE(this, "String.prototype.link");
920   return "<a href=\"" + HtmlEscape(s) + "\">" + TO_STRING_INLINE(this) + "</a>";
921 }
922
923
924 // ES6 draft, revision 26 (2014-07-18), section B.2.3.11
925 function StringSmall() {
926   CHECK_OBJECT_COERCIBLE(this, "String.prototype.small");
927   return "<small>" + TO_STRING_INLINE(this) + "</small>";
928 }
929
930
931 // ES6 draft, revision 26 (2014-07-18), section B.2.3.12
932 function StringStrike() {
933   CHECK_OBJECT_COERCIBLE(this, "String.prototype.strike");
934   return "<strike>" + TO_STRING_INLINE(this) + "</strike>";
935 }
936
937
938 // ES6 draft, revision 26 (2014-07-18), section B.2.3.13
939 function StringSub() {
940   CHECK_OBJECT_COERCIBLE(this, "String.prototype.sub");
941   return "<sub>" + TO_STRING_INLINE(this) + "</sub>";
942 }
943
944
945 // ES6 draft, revision 26 (2014-07-18), section B.2.3.14
946 function StringSup() {
947   CHECK_OBJECT_COERCIBLE(this, "String.prototype.sup");
948   return "<sup>" + TO_STRING_INLINE(this) + "</sup>";
949 }
950
951 // ES6 draft 01-20-14, section 21.1.3.13
952 function StringRepeat(count) {
953   CHECK_OBJECT_COERCIBLE(this, "String.prototype.repeat");
954
955   var s = TO_STRING_INLINE(this);
956   var n = $toInteger(count);
957   // The maximum string length is stored in a smi, so a longer repeat
958   // must result in a range error.
959   if (n < 0 || n > %_MaxSmi()) throw MakeRangeError(kInvalidCountValue);
960
961   var r = "";
962   while (true) {
963     if (n & 1) r += s;
964     n >>= 1;
965     if (n === 0) return r;
966     s += s;
967   }
968 }
969
970
971 // ES6 draft 04-05-14, section 21.1.3.18
972 function StringStartsWith(searchString /* position */) {  // length == 1
973   CHECK_OBJECT_COERCIBLE(this, "String.prototype.startsWith");
974
975   var s = TO_STRING_INLINE(this);
976
977   if (IS_REGEXP(searchString)) {
978     throw MakeTypeError(kFirstArgumentNotRegExp, "String.prototype.startsWith");
979   }
980
981   var ss = TO_STRING_INLINE(searchString);
982   var pos = 0;
983   if (%_ArgumentsLength() > 1) {
984     pos = %_Arguments(1);  // position
985     pos = $toInteger(pos);
986   }
987
988   var s_len = s.length;
989   var start = MathMin(MathMax(pos, 0), s_len);
990   var ss_len = ss.length;
991   if (ss_len + start > s_len) {
992     return false;
993   }
994
995   return %StringIndexOf(s, ss, start) === start;
996 }
997
998
999 // ES6 draft 04-05-14, section 21.1.3.7
1000 function StringEndsWith(searchString /* position */) {  // length == 1
1001   CHECK_OBJECT_COERCIBLE(this, "String.prototype.endsWith");
1002
1003   var s = TO_STRING_INLINE(this);
1004
1005   if (IS_REGEXP(searchString)) {
1006     throw MakeTypeError(kFirstArgumentNotRegExp, "String.prototype.endsWith");
1007   }
1008
1009   var ss = TO_STRING_INLINE(searchString);
1010   var s_len = s.length;
1011   var pos = s_len;
1012   if (%_ArgumentsLength() > 1) {
1013     var arg = %_Arguments(1);  // position
1014     if (!IS_UNDEFINED(arg)) {
1015       pos = $toInteger(arg);
1016     }
1017   }
1018
1019   var end = MathMin(MathMax(pos, 0), s_len);
1020   var ss_len = ss.length;
1021   var start = end - ss_len;
1022   if (start < 0) {
1023     return false;
1024   }
1025
1026   return %StringLastIndexOf(s, ss, start) === start;
1027 }
1028
1029
1030 // ES6 draft 04-05-14, section 21.1.3.6
1031 function StringIncludes(searchString /* position */) {  // length == 1
1032   CHECK_OBJECT_COERCIBLE(this, "String.prototype.includes");
1033
1034   var string = TO_STRING_INLINE(this);
1035
1036   if (IS_REGEXP(searchString)) {
1037     throw MakeTypeError(kFirstArgumentNotRegExp, "String.prototype.includes");
1038   }
1039
1040   searchString = TO_STRING_INLINE(searchString);
1041   var pos = 0;
1042   if (%_ArgumentsLength() > 1) {
1043     pos = %_Arguments(1);  // position
1044     pos = TO_INTEGER(pos);
1045   }
1046
1047   var stringLength = string.length;
1048   if (pos < 0) pos = 0;
1049   if (pos > stringLength) pos = stringLength;
1050   var searchStringLength = searchString.length;
1051
1052   if (searchStringLength + pos > stringLength) {
1053     return false;
1054   }
1055
1056   return %StringIndexOf(string, searchString, pos) !== -1;
1057 }
1058
1059
1060 // ES6 Draft 05-22-2014, section 21.1.3.3
1061 function StringCodePointAt(pos) {
1062   CHECK_OBJECT_COERCIBLE(this, "String.prototype.codePointAt");
1063
1064   var string = TO_STRING_INLINE(this);
1065   var size = string.length;
1066   pos = TO_INTEGER(pos);
1067   if (pos < 0 || pos >= size) {
1068     return UNDEFINED;
1069   }
1070   var first = %_StringCharCodeAt(string, pos);
1071   if (first < 0xD800 || first > 0xDBFF || pos + 1 == size) {
1072     return first;
1073   }
1074   var second = %_StringCharCodeAt(string, pos + 1);
1075   if (second < 0xDC00 || second > 0xDFFF) {
1076     return first;
1077   }
1078   return (first - 0xD800) * 0x400 + second + 0x2400;
1079 }
1080
1081
1082 // ES6 Draft 05-22-2014, section 21.1.2.2
1083 function StringFromCodePoint(_) {  // length = 1
1084   var code;
1085   var length = %_ArgumentsLength();
1086   var index;
1087   var result = "";
1088   for (index = 0; index < length; index++) {
1089     code = %_Arguments(index);
1090     if (!%_IsSmi(code)) {
1091       code = $toNumber(code);
1092     }
1093     if (code < 0 || code > 0x10FFFF || code !== TO_INTEGER(code)) {
1094       throw MakeRangeError(kInvalidCodePoint, code);
1095     }
1096     if (code <= 0xFFFF) {
1097       result += %_StringCharFromCode(code);
1098     } else {
1099       code -= 0x10000;
1100       result += %_StringCharFromCode((code >>> 10) & 0x3FF | 0xD800);
1101       result += %_StringCharFromCode(code & 0x3FF | 0xDC00);
1102     }
1103   }
1104   return result;
1105 }
1106
1107
1108 // -------------------------------------------------------------------
1109 // String methods related to templates
1110
1111 // ES6 Draft 03-17-2015, section 21.1.2.4
1112 function StringRaw(callSite) {
1113   // TODO(caitp): Use rest parameters when implemented
1114   var numberOfSubstitutions = %_ArgumentsLength();
1115   var cooked = $toObject(callSite);
1116   var raw = $toObject(cooked.raw);
1117   var literalSegments = $toLength(raw.length);
1118   if (literalSegments <= 0) return "";
1119
1120   var result = $toString(raw[0]);
1121
1122   for (var i = 1; i < literalSegments; ++i) {
1123     if (i < numberOfSubstitutions) {
1124       result += $toString(%_Arguments(i));
1125     }
1126     result += $toString(raw[i]);
1127   }
1128
1129   return result;
1130 }
1131
1132 // -------------------------------------------------------------------
1133
1134 // Set the String function and constructor.
1135 %SetCode(GlobalString, StringConstructor);
1136 %FunctionSetPrototype(GlobalString, new GlobalString());
1137
1138 // Set up the constructor property on the String prototype object.
1139 %AddNamedProperty(
1140     GlobalString.prototype, "constructor", GlobalString, DONT_ENUM);
1141
1142 // Set up the non-enumerable functions on the String object.
1143 utils.InstallFunctions(GlobalString, DONT_ENUM, [
1144   "fromCharCode", StringFromCharCode,
1145   "fromCodePoint", StringFromCodePoint,
1146   "raw", StringRaw
1147 ]);
1148
1149 // Set up the non-enumerable functions on the String prototype object.
1150 utils.InstallFunctions(GlobalString.prototype, DONT_ENUM, [
1151   "valueOf", StringValueOf,
1152   "toString", StringToString,
1153   "charAt", StringCharAtJS,
1154   "charCodeAt", StringCharCodeAtJS,
1155   "codePointAt", StringCodePointAt,
1156   "concat", StringConcat,
1157   "endsWith", StringEndsWith,
1158   "includes", StringIncludes,
1159   "indexOf", StringIndexOfJS,
1160   "lastIndexOf", StringLastIndexOfJS,
1161   "localeCompare", StringLocaleCompareJS,
1162   "match", StringMatchJS,
1163   "normalize", StringNormalizeJS,
1164   "repeat", StringRepeat,
1165   "replace", StringReplace,
1166   "search", StringSearch,
1167   "slice", StringSlice,
1168   "split", StringSplitJS,
1169   "substring", StringSubstring,
1170   "substr", StringSubstr,
1171   "startsWith", StringStartsWith,
1172   "toLowerCase", StringToLowerCaseJS,
1173   "toLocaleLowerCase", StringToLocaleLowerCase,
1174   "toUpperCase", StringToUpperCaseJS,
1175   "toLocaleUpperCase", StringToLocaleUpperCase,
1176   "trim", StringTrimJS,
1177   "trimLeft", StringTrimLeft,
1178   "trimRight", StringTrimRight,
1179
1180   "link", StringLink,
1181   "anchor", StringAnchor,
1182   "fontcolor", StringFontcolor,
1183   "fontsize", StringFontsize,
1184   "big", StringBig,
1185   "blink", StringBlink,
1186   "bold", StringBold,
1187   "fixed", StringFixed,
1188   "italics", StringItalics,
1189   "small", StringSmall,
1190   "strike", StringStrike,
1191   "sub", StringSub,
1192   "sup", StringSup
1193 ]);
1194
1195 // -------------------------------------------------------------------
1196 // Exports
1197
1198 utils.Export(function(to) {
1199   to.StringCharAt = StringCharAtJS;
1200   to.StringIndexOf = StringIndexOfJS;
1201   to.StringLastIndexOf = StringLastIndexOfJS;
1202   to.StringMatch = StringMatchJS;
1203   to.StringReplace = StringReplace;
1204   to.StringSplit = StringSplitJS;
1205   to.StringSubstr = StringSubstr;
1206   to.StringSubstring = StringSubstring;
1207 });
1208
1209 })