1 /*
   2  * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.nashorn.internal.objects;
  27 
  28 import static jdk.nashorn.internal.runtime.ECMAErrors.typeError;
  29 import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
  30 
  31 import java.lang.invoke.MethodHandle;
  32 import java.util.ArrayList;
  33 import java.util.Arrays;
  34 import java.util.List;
  35 import java.util.concurrent.Callable;
  36 
  37 import jdk.nashorn.internal.objects.annotations.Attribute;
  38 import jdk.nashorn.internal.objects.annotations.Constructor;
  39 import jdk.nashorn.internal.objects.annotations.Function;
  40 import jdk.nashorn.internal.objects.annotations.Getter;
  41 import jdk.nashorn.internal.objects.annotations.Property;
  42 import jdk.nashorn.internal.objects.annotations.ScriptClass;
  43 import jdk.nashorn.internal.objects.annotations.SpecializedFunction;
  44 import jdk.nashorn.internal.objects.annotations.Where;
  45 import jdk.nashorn.internal.runtime.BitVector;
  46 import jdk.nashorn.internal.runtime.JSType;
  47 import jdk.nashorn.internal.runtime.ParserException;
  48 import jdk.nashorn.internal.runtime.PropertyMap;
  49 import jdk.nashorn.internal.runtime.ScriptFunction;
  50 import jdk.nashorn.internal.runtime.ScriptObject;
  51 import jdk.nashorn.internal.runtime.ScriptRuntime;
  52 import jdk.nashorn.internal.runtime.linker.Bootstrap;
  53 import jdk.nashorn.internal.runtime.regexp.RegExp;
  54 import jdk.nashorn.internal.runtime.regexp.RegExpFactory;
  55 import jdk.nashorn.internal.runtime.regexp.RegExpMatcher;
  56 import jdk.nashorn.internal.runtime.regexp.RegExpResult;
  57 
  58 /**
  59  * ECMA 15.10 RegExp Objects.
  60  */
  61 @ScriptClass("RegExp")
  62 public final class NativeRegExp extends ScriptObject {
  63     /** ECMA 15.10.7.5 lastIndex property */
  64     @Property(attributes = Attribute.NOT_ENUMERABLE | Attribute.NOT_CONFIGURABLE)
  65     public Object lastIndex;
  66 
  67     /** Compiled regexp */
  68     private RegExp regexp;
  69 
  70     // Reference to global object needed to support static RegExp properties
  71     private final Global globalObject;
  72 
  73     // initialized by nasgen
  74     private static PropertyMap $nasgenmap$;
  75 
  76     private NativeRegExp(final Global global) {
  77         super(global.getRegExpPrototype(), $nasgenmap$);
  78         this.globalObject = global;
  79     }
  80 
  81     NativeRegExp(final String input, final String flagString, final Global global, final ScriptObject proto) {
  82         super(proto, $nasgenmap$);
  83         try {
  84             this.regexp = RegExpFactory.create(input, flagString);
  85         } catch (final ParserException e) {
  86             // translate it as SyntaxError object and throw it
  87             e.throwAsEcmaException();
  88             throw new AssertionError(); //guard against null warnings below
  89         }
  90         this.globalObject = global;
  91         this.setLastIndex(0);
  92     }
  93 
  94     NativeRegExp(final String input, final String flagString, final Global global) {
  95         this(input, flagString, global, global.getRegExpPrototype());
  96     }
  97 
  98     NativeRegExp(final String input, final String flagString) {
  99         this(input, flagString, Global.instance());
 100     }
 101 
 102     NativeRegExp(final String string, final Global global) {
 103         this(string, "", global);
 104     }
 105 
 106     NativeRegExp(final String string) {
 107         this(string, Global.instance());
 108     }
 109 
 110     NativeRegExp(final NativeRegExp regExp) {
 111         this(Global.instance());
 112         this.lastIndex  = regExp.getLastIndexObject();
 113         this.regexp      = regExp.getRegExp();
 114     }
 115 
 116     @Override
 117     public String getClassName() {
 118         return "RegExp";
 119     }
 120 
 121     /**
 122      * ECMA 15.10.4
 123      *
 124      * Constructor
 125      *
 126      * @param isNew is the new operator used for instantiating this regexp
 127      * @param self  self reference
 128      * @param args  arguments (optional: pattern and flags)
 129      * @return new NativeRegExp
 130      */
 131     @Constructor(arity = 2)
 132     public static NativeRegExp constructor(final boolean isNew, final Object self, final Object... args) {
 133         if (args.length > 1) {
 134             return newRegExp(args[0], args[1]);
 135         } else if (args.length > 0) {
 136             return newRegExp(args[0], UNDEFINED);
 137         }
 138 
 139         return newRegExp(UNDEFINED, UNDEFINED);
 140     }
 141 
 142     /**
 143      * ECMA 15.10.4
 144      *
 145      * Constructor - specialized version, no args, empty regexp
 146      *
 147      * @param isNew is the new operator used for instantiating this regexp
 148      * @param self  self reference
 149      * @return new NativeRegExp
 150      */
 151     @SpecializedFunction(isConstructor=true)
 152     public static NativeRegExp constructor(final boolean isNew, final Object self) {
 153         return new NativeRegExp("", "");
 154     }
 155 
 156     /**
 157      * ECMA 15.10.4
 158      *
 159      * Constructor - specialized version, pattern, no flags
 160      *
 161      * @param isNew is the new operator used for instantiating this regexp
 162      * @param self  self reference
 163      * @param pattern pattern
 164      * @return new NativeRegExp
 165      */
 166     @SpecializedFunction(isConstructor=true)
 167     public static NativeRegExp constructor(final boolean isNew, final Object self, final Object pattern) {
 168         return newRegExp(pattern, UNDEFINED);
 169     }
 170 
 171     /**
 172      * ECMA 15.10.4
 173      *
 174      * Constructor - specialized version, pattern and flags
 175      *
 176      * @param isNew is the new operator used for instantiating this regexp
 177      * @param self  self reference
 178      * @param pattern pattern
 179      * @param flags  flags
 180      * @return new NativeRegExp
 181      */
 182     @SpecializedFunction(isConstructor=true)
 183     public static NativeRegExp constructor(final boolean isNew, final Object self, final Object pattern, final Object flags) {
 184         return newRegExp(pattern, flags);
 185     }
 186 
 187     /**
 188      * External constructor used in generated code, which explains the public access
 189      *
 190      * @param regexp regexp
 191      * @param flags  flags
 192      * @return new NativeRegExp
 193      */
 194     public static NativeRegExp newRegExp(final Object regexp, final Object flags) {
 195         String  patternString = "";
 196         String  flagString    = "";
 197 
 198         if (regexp != UNDEFINED) {
 199             if (regexp instanceof NativeRegExp) {
 200                 if (flags != UNDEFINED) {
 201                     throw typeError("regex.cant.supply.flags");
 202                 }
 203                 return (NativeRegExp)regexp; // 15.10.3.1 - undefined flags and regexp as
 204             }
 205             patternString = JSType.toString(regexp);
 206         }
 207 
 208         if (flags != UNDEFINED) {
 209             flagString = JSType.toString(flags);
 210         }
 211 
 212         return new NativeRegExp(patternString, flagString);
 213     }
 214 
 215     /**
 216      * Build a regexp that matches {@code string} as-is. All meta-characters will be escaped.
 217      *
 218      * @param string pattern string
 219      * @return flat regexp
 220      */
 221     static NativeRegExp flatRegExp(final String string) {
 222         // escape special characters
 223         StringBuilder sb = null;
 224         final int length = string.length();
 225 
 226         for (int i = 0; i < length; i++) {
 227             final char c = string.charAt(i);
 228             switch (c) {
 229                 case '^':
 230                 case '$':
 231                 case '\\':
 232                 case '.':
 233                 case '*':
 234                 case '+':
 235                 case '?':
 236                 case '(':
 237                 case ')':
 238                 case '[':
 239                 case '{':
 240                 case '|':
 241                     if (sb == null) {
 242                         sb = new StringBuilder(length * 2);
 243                         sb.append(string, 0, i);
 244                     }
 245                     sb.append('\\');
 246                     sb.append(c);
 247                     break;
 248                 default:
 249                     if (sb != null) {
 250                         sb.append(c);
 251                     }
 252                     break;
 253             }
 254         }
 255         return new NativeRegExp(sb == null ? string : sb.toString(), "");
 256     }
 257 
 258     private String getFlagString() {
 259         final StringBuilder sb = new StringBuilder(3);
 260 
 261         if (regexp.isGlobal()) {
 262             sb.append('g');
 263         }
 264         if (regexp.isIgnoreCase()) {
 265             sb.append('i');
 266         }
 267         if (regexp.isMultiline()) {
 268             sb.append('m');
 269         }
 270 
 271         return sb.toString();
 272     }
 273 
 274     @Override
 275     public String safeToString() {
 276         return "[RegExp " + toString() + "]";
 277     }
 278 
 279     @Override
 280     public String toString() {
 281         return "/" + regexp.getSource() + "/" + getFlagString();
 282     }
 283 
 284     /**
 285      * Nashorn extension: RegExp.prototype.compile - everybody implements this!
 286      *
 287      * @param self    self reference
 288      * @param pattern pattern
 289      * @param flags   flags
 290      * @return new NativeRegExp
 291      */
 292     @Function(attributes = Attribute.NOT_ENUMERABLE)
 293     public static ScriptObject compile(final Object self, final Object pattern, final Object flags) {
 294         final NativeRegExp regExp   = checkRegExp(self);
 295         final NativeRegExp compiled = newRegExp(pattern, flags);
 296         // copy over regexp to 'self'
 297         regExp.setRegExp(compiled.getRegExp());
 298 
 299         // Some implementations return undefined. Some return 'self'. Since return
 300         // value is most likely be ignored, we can play safe and return 'self'.
 301         return regExp;
 302     }
 303 
 304     /**
 305      * ECMA 15.10.6.2 RegExp.prototype.exec(string)
 306      *
 307      * @param self   self reference
 308      * @param string string to match against regexp
 309      * @return array containing the matches or {@code null} if no match
 310      */
 311     @Function(attributes = Attribute.NOT_ENUMERABLE)
 312     public static ScriptObject exec(final Object self, final Object string) {
 313         return checkRegExp(self).exec(JSType.toString(string));
 314     }
 315 
 316     /**
 317      * ECMA 15.10.6.3 RegExp.prototype.test(string)
 318      *
 319      * @param self   self reference
 320      * @param string string to test for matches against regexp
 321      * @return true if matches found, false otherwise
 322      */
 323     @Function(attributes = Attribute.NOT_ENUMERABLE)
 324     public static boolean test(final Object self, final Object string) {
 325         return checkRegExp(self).test(JSType.toString(string));
 326     }
 327 
 328     /**
 329      * ECMA 15.10.6.4 RegExp.prototype.toString()
 330      *
 331      * @param self self reference
 332      * @return string version of regexp
 333      */
 334     @Function(attributes = Attribute.NOT_ENUMERABLE)
 335     public static String toString(final Object self) {
 336         return checkRegExp(self).toString();
 337     }
 338 
 339     /**
 340      * ECMA 15.10.7.1 source
 341      *
 342      * @param self self reference
 343      * @return the input string for the regexp
 344      */
 345     @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
 346     public static Object source(final Object self) {
 347         return checkRegExp(self).getRegExp().getSource();
 348     }
 349 
 350     /**
 351      * ECMA 15.10.7.2 global
 352      *
 353      * @param self self reference
 354      * @return true if this regexp is flagged global, false otherwise
 355      */
 356     @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
 357     public static Object global(final Object self) {
 358         return checkRegExp(self).getRegExp().isGlobal();
 359     }
 360 
 361     /**
 362      * ECMA 15.10.7.3 ignoreCase
 363      *
 364      * @param self self reference
 365      * @return true if this regexp if flagged to ignore case, false otherwise
 366      */
 367     @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
 368     public static Object ignoreCase(final Object self) {
 369         return checkRegExp(self).getRegExp().isIgnoreCase();
 370     }
 371 
 372     /**
 373      * ECMA 15.10.7.4 multiline
 374      *
 375      * @param self self reference
 376      * @return true if this regexp is flagged to be multiline, false otherwise
 377      */
 378     @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
 379     public static Object multiline(final Object self) {
 380         return checkRegExp(self).getRegExp().isMultiline();
 381     }
 382 
 383     /**
 384      * Getter for non-standard RegExp.input property.
 385      * @param self self object
 386      * @return last regexp input
 387      */
 388     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "input")
 389     public static Object getLastInput(final Object self) {
 390         final RegExpResult match = Global.instance().getLastRegExpResult();
 391         return match == null ? "" : match.getInput();
 392     }
 393 
 394     /**
 395      * Getter for non-standard RegExp.multiline property.
 396      * @param self self object
 397      * @return last regexp input
 398      */
 399     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "multiline")
 400     public static Object getLastMultiline(final Object self) {
 401         return false; // doesn't ever seem to become true and isn't documented anyhwere
 402     }
 403 
 404     /**
 405      * Getter for non-standard RegExp.lastMatch property.
 406      * @param self self object
 407      * @return last regexp input
 408      */
 409     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "lastMatch")
 410     public static Object getLastMatch(final Object self) {
 411         final RegExpResult match = Global.instance().getLastRegExpResult();
 412         return match == null ? "" : match.getGroup(0);
 413     }
 414 
 415     /**
 416      * Getter for non-standard RegExp.lastParen property.
 417      * @param self self object
 418      * @return last regexp input
 419      */
 420     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "lastParen")
 421     public static Object getLastParen(final Object self) {
 422         final RegExpResult match = Global.instance().getLastRegExpResult();
 423         return match == null ? "" : match.getLastParen();
 424     }
 425 
 426     /**
 427      * Getter for non-standard RegExp.leftContext property.
 428      * @param self self object
 429      * @return last regexp input
 430      */
 431     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "leftContext")
 432     public static Object getLeftContext(final Object self) {
 433         final RegExpResult match = Global.instance().getLastRegExpResult();
 434         return match == null ? "" : match.getInput().substring(0, match.getIndex());
 435     }
 436 
 437     /**
 438      * Getter for non-standard RegExp.rightContext property.
 439      * @param self self object
 440      * @return last regexp input
 441      */
 442     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "rightContext")
 443     public static Object getRightContext(final Object self) {
 444         final RegExpResult match = Global.instance().getLastRegExpResult();
 445         return match == null ? "" : match.getInput().substring(match.getIndex() + match.length());
 446     }
 447 
 448     /**
 449      * Getter for non-standard RegExp.$1 property.
 450      * @param self self object
 451      * @return last regexp input
 452      */
 453     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$1")
 454     public static Object getGroup1(final Object self) {
 455         final RegExpResult match = Global.instance().getLastRegExpResult();
 456         return match == null ? "" : match.getGroup(1);
 457     }
 458 
 459     /**
 460      * Getter for non-standard RegExp.$2 property.
 461      * @param self self object
 462      * @return last regexp input
 463      */
 464     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$2")
 465     public static Object getGroup2(final Object self) {
 466         final RegExpResult match = Global.instance().getLastRegExpResult();
 467         return match == null ? "" : match.getGroup(2);
 468     }
 469 
 470     /**
 471      * Getter for non-standard RegExp.$3 property.
 472      * @param self self object
 473      * @return last regexp input
 474      */
 475     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$3")
 476     public static Object getGroup3(final Object self) {
 477         final RegExpResult match = Global.instance().getLastRegExpResult();
 478         return match == null ? "" : match.getGroup(3);
 479     }
 480 
 481     /**
 482      * Getter for non-standard RegExp.$4 property.
 483      * @param self self object
 484      * @return last regexp input
 485      */
 486     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$4")
 487     public static Object getGroup4(final Object self) {
 488         final RegExpResult match = Global.instance().getLastRegExpResult();
 489         return match == null ? "" : match.getGroup(4);
 490     }
 491 
 492     /**
 493      * Getter for non-standard RegExp.$5 property.
 494      * @param self self object
 495      * @return last regexp input
 496      */
 497     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$5")
 498     public static Object getGroup5(final Object self) {
 499         final RegExpResult match = Global.instance().getLastRegExpResult();
 500         return match == null ? "" : match.getGroup(5);
 501     }
 502 
 503     /**
 504      * Getter for non-standard RegExp.$6 property.
 505      * @param self self object
 506      * @return last regexp input
 507      */
 508     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$6")
 509     public static Object getGroup6(final Object self) {
 510         final RegExpResult match = Global.instance().getLastRegExpResult();
 511         return match == null ? "" : match.getGroup(6);
 512     }
 513 
 514     /**
 515      * Getter for non-standard RegExp.$7 property.
 516      * @param self self object
 517      * @return last regexp input
 518      */
 519     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$7")
 520     public static Object getGroup7(final Object self) {
 521         final RegExpResult match = Global.instance().getLastRegExpResult();
 522         return match == null ? "" : match.getGroup(7);
 523     }
 524 
 525     /**
 526      * Getter for non-standard RegExp.$8 property.
 527      * @param self self object
 528      * @return last regexp input
 529      */
 530     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$8")
 531     public static Object getGroup8(final Object self) {
 532         final RegExpResult match = Global.instance().getLastRegExpResult();
 533         return match == null ? "" : match.getGroup(8);
 534     }
 535 
 536     /**
 537      * Getter for non-standard RegExp.$9 property.
 538      * @param self self object
 539      * @return last regexp input
 540      */
 541     @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$9")
 542     public static Object getGroup9(final Object self) {
 543         final RegExpResult match = Global.instance().getLastRegExpResult();
 544         return match == null ? "" : match.getGroup(9);
 545     }
 546 
 547     private RegExpResult execInner(final String string) {
 548         final boolean isGlobal = regexp.isGlobal();
 549         int start = getLastIndex();
 550         if (!isGlobal) {
 551             start = 0;
 552         }
 553 
 554         if (start < 0 || start > string.length()) {
 555             if (isGlobal) {
 556                 setLastIndex(0);
 557             }
 558             return null;
 559         }
 560 
 561         final RegExpMatcher matcher = regexp.match(string);
 562         if (matcher == null || !matcher.search(start)) {
 563             if (isGlobal) {
 564                 setLastIndex(0);
 565             }
 566             return null;
 567         }
 568 
 569         if (isGlobal) {
 570             setLastIndex(matcher.end());
 571         }
 572 
 573         final RegExpResult match = new RegExpResult(string, matcher.start(), groups(matcher));
 574         globalObject.setLastRegExpResult(match);
 575         return match;
 576     }
 577 
 578     // String.prototype.split method ignores the global flag and should not update lastIndex property.
 579     private RegExpResult execSplit(final String string, final int start) {
 580         if (start < 0 || start > string.length()) {
 581             return null;
 582         }
 583 
 584         final RegExpMatcher matcher = regexp.match(string);
 585         if (matcher == null || !matcher.search(start)) {
 586             return null;
 587         }
 588 
 589         final RegExpResult match = new RegExpResult(string, matcher.start(), groups(matcher));
 590         globalObject.setLastRegExpResult(match);
 591         return match;
 592     }
 593 
 594     /**
 595      * Convert java.util.regex.Matcher groups to JavaScript groups.
 596      * That is, replace null and groups that didn't match with undefined.
 597      */
 598     private Object[] groups(final RegExpMatcher matcher) {
 599         final int groupCount = matcher.groupCount();
 600         final Object[] groups = new Object[groupCount + 1];
 601         final BitVector groupsInNegativeLookahead  = regexp.getGroupsInNegativeLookahead();
 602 
 603         for (int i = 0, lastGroupStart = matcher.start(); i <= groupCount; i++) {
 604             final int groupStart = matcher.start(i);
 605             if (lastGroupStart > groupStart
 606                     || groupsInNegativeLookahead != null && groupsInNegativeLookahead.isSet(i)) {
 607                 // (1) ECMA 15.10.2.5 NOTE 3: need to clear Atom's captures each time Atom is repeated.
 608                 // (2) ECMA 15.10.2.8 NOTE 3: Backreferences to captures in (?!Disjunction) from elsewhere
 609                 // in the pattern always return undefined because the negative lookahead must fail.
 610                 groups[i] = UNDEFINED;
 611                 continue;
 612             }
 613             final String group = matcher.group(i);
 614             groups[i] = group == null ? UNDEFINED : group;
 615             lastGroupStart = groupStart;
 616         }
 617         return groups;
 618     }
 619 
 620     /**
 621      * Executes a search for a match within a string based on a regular
 622      * expression. It returns an array of information or null if no match is
 623      * found.
 624      *
 625      * @param string String to match.
 626      * @return NativeArray of matches, string or null.
 627      */
 628     public NativeRegExpExecResult exec(final String string) {
 629         final RegExpResult match = execInner(string);
 630 
 631         if (match == null) {
 632             return null;
 633         }
 634 
 635         return new NativeRegExpExecResult(match, globalObject);
 636     }
 637 
 638     /**
 639      * Executes a search for a match within a string based on a regular
 640      * expression.
 641      *
 642      * @param string String to match.
 643      * @return True if a match is found.
 644      */
 645     public boolean test(final String string) {
 646         return execInner(string) != null;
 647     }
 648 
 649     /**
 650      * Searches and replaces the regular expression portion (match) with the
 651      * replaced text instead. For the "replacement text" parameter, you can use
 652      * the keywords $1 to $2 to replace the original text with values from
 653      * sub-patterns defined within the main pattern.
 654      *
 655      * @param string String to match.
 656      * @param replacement Replacement string.
 657      * @return String with substitutions.
 658      */
 659     String replace(final String string, final String replacement, final ScriptFunction function) throws Throwable {
 660         final RegExpMatcher matcher = regexp.match(string);
 661 
 662         if (matcher == null) {
 663             return string;
 664         }
 665 
 666         if (!regexp.isGlobal()) {
 667             if (!matcher.search(0)) {
 668                 return string;
 669             }
 670 
 671             final StringBuilder sb = new StringBuilder();
 672             sb.append(string, 0, matcher.start());
 673 
 674             if (function != null) {
 675                 final Object self = function.isStrict() ? UNDEFINED : Global.instance();
 676                 sb.append(callReplaceValue(getReplaceValueInvoker(), function, self, matcher, string));
 677             } else {
 678                 appendReplacement(matcher, string, replacement, sb);
 679             }
 680             sb.append(string, matcher.end(), string.length());
 681             return sb.toString();
 682         }
 683 
 684         setLastIndex(0);
 685 
 686         if (!matcher.search(0)) {
 687             return string;
 688         }
 689 
 690         int thisIndex = 0;
 691         int previousLastIndex = 0;
 692         final StringBuilder sb = new StringBuilder();
 693 
 694         final MethodHandle invoker = function == null ? null : getReplaceValueInvoker();
 695         final Object self = function == null || function.isStrict() ? UNDEFINED : Global.instance();
 696 
 697         do {
 698             sb.append(string, thisIndex, matcher.start());
 699             if (function != null) {
 700                 sb.append(callReplaceValue(invoker, function, self, matcher, string));
 701             } else {
 702                 appendReplacement(matcher, string, replacement, sb);
 703             }
 704 
 705             thisIndex = matcher.end();
 706             if (thisIndex == string.length() && matcher.start() == matcher.end()) {
 707                 // Avoid getting empty match at end of string twice
 708                 break;
 709             }
 710 
 711             // ECMA 15.5.4.10 String.prototype.match(regexp)
 712             if (thisIndex == previousLastIndex) {
 713                 setLastIndex(thisIndex + 1);
 714                 previousLastIndex = thisIndex + 1;
 715             } else {
 716                 previousLastIndex = thisIndex;
 717             }
 718         } while (previousLastIndex <= string.length() && matcher.search(previousLastIndex));
 719 
 720         sb.append(string, thisIndex, string.length());
 721 
 722         return sb.toString();
 723     }
 724 
 725     private void appendReplacement(final RegExpMatcher matcher, final String text, final String replacement, final StringBuilder sb) {
 726         /*
 727          * Process substitution patterns:
 728          *
 729          * $$ -> $
 730          * $& -> the matched substring
 731          * $` -> the portion of string that preceeds matched substring
 732          * $' -> the portion of string that follows the matched substring
 733          * $n -> the nth capture, where n is [1-9] and $n is NOT followed by a decimal digit
 734          * $nn -> the nnth capture, where nn is a two digit decimal number [01-99].
 735          */
 736 
 737         int cursor = 0;
 738         Object[] groups = null;
 739 
 740         while (cursor < replacement.length()) {
 741             char nextChar = replacement.charAt(cursor);
 742             if (nextChar == '$') {
 743                 // Skip past $
 744                 cursor++;
 745                 if (cursor == replacement.length()) {
 746                     // nothing after "$"
 747                     sb.append('$');
 748                     break;
 749                 }
 750 
 751                 nextChar = replacement.charAt(cursor);
 752                 final int firstDigit = nextChar - '0';
 753 
 754                 if (firstDigit >= 0 && firstDigit <= 9 && firstDigit <= matcher.groupCount()) {
 755                     // $0 is not supported, but $01 is. implementation-defined: if n>m, ignore second digit.
 756                     int refNum = firstDigit;
 757                     cursor++;
 758                     if (cursor < replacement.length() && firstDigit < matcher.groupCount()) {
 759                         final int secondDigit = replacement.charAt(cursor) - '0';
 760                         if (secondDigit >= 0 && secondDigit <= 9) {
 761                             final int newRefNum = firstDigit * 10 + secondDigit;
 762                             if (newRefNum <= matcher.groupCount() && newRefNum > 0) {
 763                                 // $nn ($01-$99)
 764                                 refNum = newRefNum;
 765                                 cursor++;
 766                             }
 767                         }
 768                     }
 769                     if (refNum > 0) {
 770                         if (groups == null) {
 771                             groups = groups(matcher);
 772                         }
 773                         // Append group if matched.
 774                         if (groups[refNum] != UNDEFINED) {
 775                             sb.append((String) groups[refNum]);
 776                         }
 777                     } else { // $0. ignore.
 778                         assert refNum == 0;
 779                         sb.append("$0");
 780                     }
 781                 } else if (nextChar == '$') {
 782                     sb.append('$');
 783                     cursor++;
 784                 } else if (nextChar == '&') {
 785                     sb.append(matcher.group());
 786                     cursor++;
 787                 } else if (nextChar == '`') {
 788                     sb.append(text, 0, matcher.start());
 789                     cursor++;
 790                 } else if (nextChar == '\'') {
 791                     sb.append(text, matcher.end(), text.length());
 792                     cursor++;
 793                 } else {
 794                     // unknown substitution or $n with n>m. skip.
 795                     sb.append('$');
 796                 }
 797             } else {
 798                 sb.append(nextChar);
 799                 cursor++;
 800             }
 801         }
 802     }
 803 
 804     private static final Object REPLACE_VALUE = new Object();
 805 
 806     private static final MethodHandle getReplaceValueInvoker() {
 807         return Global.instance().getDynamicInvoker(REPLACE_VALUE,
 808                 new Callable<MethodHandle>() {
 809                     @Override
 810                     public MethodHandle call() {
 811                         return Bootstrap.createDynamicInvoker("dyn:call", String.class, ScriptFunction.class, Object.class, Object[].class);
 812                     }
 813                 });
 814     }
 815 
 816     private String callReplaceValue(final MethodHandle invoker, final ScriptFunction function, final Object self, final RegExpMatcher matcher, final String string) throws Throwable {
 817         final Object[] groups = groups(matcher);
 818         final Object[] args   = Arrays.copyOf(groups, groups.length + 2);
 819 
 820         args[groups.length]     = matcher.start();
 821         args[groups.length + 1] = string;
 822 
 823         return (String)invoker.invokeExact(function, self, args);
 824     }
 825 
 826     /**
 827      * Breaks up a string into an array of substrings based on a regular
 828      * expression or fixed string.
 829      *
 830      * @param string String to match.
 831      * @param limit  Split limit.
 832      * @return Array of substrings.
 833      */
 834     NativeArray split(final String string, final long limit) {
 835         if (limit == 0L) {
 836             return new NativeArray();
 837         }
 838 
 839         final List<Object> matches = new ArrayList<>();
 840 
 841         RegExpResult match;
 842         final int inputLength = string.length();
 843         int splitLastLength = -1;
 844         int splitLastIndex = 0;
 845         int splitLastLastIndex = 0;
 846 
 847         while ((match = execSplit(string, splitLastIndex)) != null) {
 848             splitLastIndex = match.getIndex() + match.length();
 849 
 850             if (splitLastIndex > splitLastLastIndex) {
 851                 matches.add(string.substring(splitLastLastIndex, match.getIndex()));
 852                 final Object[] groups = match.getGroups();
 853                 if (groups.length > 1 && match.getIndex() < inputLength) {
 854                     for (int index = 1; index < groups.length && matches.size() < limit; index++) {
 855                         matches.add(groups[index]);
 856                     }
 857                 }
 858 
 859                 splitLastLength = match.length();
 860 
 861                 if (matches.size() >= limit) {
 862                     break;
 863                 }
 864             }
 865 
 866             // bump the index to avoid infinite loop
 867             if (splitLastIndex == splitLastLastIndex) {
 868                 splitLastIndex++;
 869             } else {
 870                 splitLastLastIndex = splitLastIndex;
 871             }
 872         }
 873 
 874         if (matches.size() < limit) {
 875             // check special case if we need to append an empty string at the
 876             // end of the match
 877             // if the lastIndex was the entire string
 878             if (splitLastLastIndex == string.length()) {
 879                 if (splitLastLength > 0 || execSplit("", 0) == null) {
 880                     matches.add("");
 881                 }
 882             } else {
 883                 matches.add(string.substring(splitLastLastIndex, inputLength));
 884             }
 885         }
 886 
 887         return new NativeArray(matches.toArray());
 888     }
 889 
 890     /**
 891      * Tests for a match in a string. It returns the index of the match, or -1
 892      * if not found.
 893      *
 894      * @param string String to match.
 895      * @return Index of match.
 896      */
 897     int search(final String string) {
 898         final RegExpResult match = execInner(string);
 899 
 900         if (match == null) {
 901             return -1;
 902         }
 903 
 904         return match.getIndex();
 905     }
 906 
 907     /**
 908      * Fast lastIndex getter
 909      * @return last index property as int
 910      */
 911     public int getLastIndex() {
 912         return JSType.toInteger(lastIndex);
 913     }
 914 
 915     /**
 916      * Fast lastIndex getter
 917      * @return last index property as boxed integer
 918      */
 919     public Object getLastIndexObject() {
 920         return lastIndex;
 921     }
 922 
 923     /**
 924      * Fast lastIndex setter
 925      * @param lastIndex lastIndex
 926      */
 927     public void setLastIndex(final int lastIndex) {
 928         this.lastIndex = JSType.toObject(lastIndex);
 929     }
 930 
 931     private static NativeRegExp checkRegExp(final Object self) {
 932         if (self instanceof NativeRegExp) {
 933             return (NativeRegExp)self;
 934         } else if (self != null && self == Global.instance().getRegExpPrototype()) {
 935             return Global.instance().getDefaultRegExp();
 936         } else {
 937             throw typeError("not.a.regexp", ScriptRuntime.safeToString(self));
 938         }
 939     }
 940 
 941     boolean getGlobal() {
 942         return regexp.isGlobal();
 943     }
 944 
 945     private RegExp getRegExp() {
 946         return regexp;
 947     }
 948 
 949     private void setRegExp(final RegExp regexp) {
 950         this.regexp = regexp;
 951     }
 952 
 953 }