nashorn/src/jdk/nashorn/internal/objects/NativeRegExp.java
author sundar
Mon, 11 Feb 2013 21:26:06 +0530
changeset 16226 0e4f37e6cc40
parent 16223 dff592a332a4
child 16240 e1468b33e201
permissions -rw-r--r--
8007915: Nashorn IR, codegen, parser packages and Context instance should be inaccessible to user code Reviewed-by: lagergren, jlaskey, attila

/*
 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.nashorn.internal.objects;

import static jdk.nashorn.internal.runtime.ECMAErrors.typeError;
import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jdk.nashorn.internal.objects.annotations.Attribute;
import jdk.nashorn.internal.objects.annotations.Constructor;
import jdk.nashorn.internal.objects.annotations.Function;
import jdk.nashorn.internal.objects.annotations.Getter;
import jdk.nashorn.internal.objects.annotations.Property;
import jdk.nashorn.internal.objects.annotations.ScriptClass;
import jdk.nashorn.internal.objects.annotations.SpecializedConstructor;
import jdk.nashorn.internal.objects.annotations.Where;
import jdk.nashorn.internal.runtime.BitVector;
import jdk.nashorn.internal.runtime.JSType;
import jdk.nashorn.internal.runtime.ParserException;
import jdk.nashorn.internal.runtime.RegExp;
import jdk.nashorn.internal.runtime.RegExpMatch;
import jdk.nashorn.internal.runtime.ScriptFunction;
import jdk.nashorn.internal.runtime.ScriptObject;
import jdk.nashorn.internal.runtime.ScriptRuntime;

/**
 * ECMA 15.10 RegExp Objects.
 */
@ScriptClass("RegExp")
public final class NativeRegExp extends ScriptObject {
    /** ECMA 15.10.7.5 lastIndex property */
    @Property(attributes = Attribute.NOT_ENUMERABLE | Attribute.NOT_CONFIGURABLE)
    public Object lastIndex;

    /** Pattern string. */
    private String input;

    /** Global search flag for this regexp. */
    private boolean global;

    /** Case insensitive flag for this regexp */
    private boolean ignoreCase;

    /** Multi-line flag for this regexp */
    private boolean multiline;

    /** Java regex pattern to use for match. We compile to one of these */
    private Pattern pattern;

    private BitVector groupsInNegativeLookahead;

    // Reference to global object needed to support static RegExp properties
    private Global globalObject;

    /*
    public NativeRegExp() {
        init();
    }*/

    NativeRegExp(final String input, final String flagString) {
        RegExp regExp = null;
        try {
            regExp = new RegExp(input, flagString);
        } catch (final ParserException e) {
            // translate it as SyntaxError object and throw it
            e.throwAsEcmaException();
            throw new AssertionError(); //guard against null warnings below
        }

        this.setLastIndex(0);
        this.input = regExp.getInput();
        this.global = regExp.isGlobal();
        this.ignoreCase = regExp.isIgnoreCase();
        this.multiline = regExp.isMultiline();
        this.pattern = regExp.getPattern();
        this.groupsInNegativeLookahead = regExp.getGroupsInNegativeLookahead();

        init();
    }

    NativeRegExp(final String string) {
        this(string, "");
    }

    NativeRegExp(final NativeRegExp regExp) {
        this.input      = regExp.getInput();
        this.global     = regExp.getGlobal();
        this.multiline  = regExp.getMultiline();
        this.ignoreCase = regExp.getIgnoreCase();
        this.lastIndex  = regExp.getLastIndexObject();
        this.pattern    = regExp.getPattern();
        this.groupsInNegativeLookahead = regExp.getGroupsInNegativeLookahead();

        init();
    }

    NativeRegExp(final Pattern pattern) {
        this.input      = pattern.pattern();
        this.multiline  = (pattern.flags() & Pattern.MULTILINE) != 0;
        this.ignoreCase = (pattern.flags() & Pattern.CASE_INSENSITIVE) != 0;
        this.lastIndex  = 0;
        this.pattern    = pattern;

        init();
    }

    @Override
    public String getClassName() {
        return "RegExp";
    }

    /**
     * ECMA 15.10.4
     *
     * Constructor
     *
     * @param isNew is the new operator used for instantiating this regexp
     * @param self  self reference
     * @param args  arguments (optional: pattern and flags)
     * @return new NativeRegExp
     */
    @Constructor(arity = 2)
    public static Object constructor(final boolean isNew, final Object self, final Object... args) {
        if (args.length > 1) {
            return newRegExp(args[0], args[1]);
        } else if (args.length > 0) {
            return newRegExp(args[0], UNDEFINED);
        }

        return newRegExp(UNDEFINED, UNDEFINED);
    }

    /**
     * ECMA 15.10.4
     *
     * Constructor - specialized version, no args, empty regexp
     *
     * @param isNew is the new operator used for instantiating this regexp
     * @param self  self reference
     * @return new NativeRegExp
     */
    @SpecializedConstructor
    public static Object constructor(final boolean isNew, final Object self) {
        return new NativeRegExp("", "");
    }

    /**
     * ECMA 15.10.4
     *
     * Constructor - specialized version, pattern, no flags
     *
     * @param isNew is the new operator used for instantiating this regexp
     * @param self  self reference
     * @param pattern pattern
     * @return new NativeRegExp
     */
    @SpecializedConstructor
    public static Object constructor(final boolean isNew, final Object self, final Object pattern) {
        return newRegExp(pattern, UNDEFINED);
    }

    /**
     * ECMA 15.10.4
     *
     * Constructor - specialized version, pattern and flags
     *
     * @param isNew is the new operator used for instantiating this regexp
     * @param self  self reference
     * @param pattern pattern
     * @param flags  flags
     * @return new NativeRegExp
     */
    @SpecializedConstructor
    public static Object constructor(final boolean isNew, final Object self, final Object pattern, final Object flags) {
        return newRegExp(pattern, flags);
    }

    /**
     * External constructor used in generated code created by {@link jdk.nashorn.internal.codegen.CodeGenerator}, which
     * explain the {@code public} access.
     *
     * @param regexp regexp
     * @param flags  flags
     * @return new NativeRegExp
     */
    public static NativeRegExp newRegExp(final Object regexp, final Object flags) {
        String  patternString = "";
        String  flagString    = "";
        boolean flagsDefined  = false;

        if (flags != UNDEFINED) {
            flagsDefined = true;
            flagString = JSType.toString(flags);
        }

        if (regexp != UNDEFINED) {
            if (regexp instanceof NativeRegExp) {
                if (!flagsDefined) {
                    return (NativeRegExp)regexp; // 15.10.3.1 - undefined flags and regexp as
                }
                typeError("regex.cant.supply.flags");
            }
            patternString = JSType.toString(regexp);
        }

        return new NativeRegExp(patternString, flagString);
    }

    private String getFlagString() {
        final StringBuilder sb = new StringBuilder();

        if (global) {
            sb.append('g');
        }
        if (ignoreCase) {
            sb.append('i');
        }
        if (multiline) {
            sb.append('m');
        }

        return sb.toString();
    }

    @Override
    public String safeToString() {
        return "[RegExp " + toString() + "]";
    }

    @Override
    public String toString() {
        return "/" + input + "/" + getFlagString();
    }

    /**
     * Nashorn extension: RegExp.prototype.compile - everybody implements this!
     *
     * @param self    self reference
     * @param pattern pattern
     * @param flags   flags
     * @return new NativeRegExp
     */
    @Function(attributes = Attribute.NOT_ENUMERABLE)
    public static Object compile(final Object self, final Object pattern, final Object flags) {
        final NativeRegExp regExp   = checkRegExp(self);
        final NativeRegExp compiled = newRegExp(pattern, flags);
        // copy over fields to 'self'
        regExp.setInput(compiled.getInput());
        regExp.setGlobal(compiled.getGlobal());
        regExp.setIgnoreCase(compiled.getIgnoreCase());
        regExp.setMultiline(compiled.getMultiline());
        regExp.setPattern(compiled.getPattern());
        regExp.setGroupsInNegativeLookahead(compiled.getGroupsInNegativeLookahead());

        // Some implementations return undefined. Some return 'self'. Since return
        // value is most likely be ignored, we can play safe and return 'self'.
        return regExp;
    }

    /**
     * ECMA 15.10.6.2 RegExp.prototype.exec(string)
     *
     * @param self   self reference
     * @param string string to match against regexp
     * @return array containing the matches or {@code null} if no match
     */
    @Function(attributes = Attribute.NOT_ENUMERABLE)
    public static Object exec(final Object self, final Object string) {
        return checkRegExp(self).exec(JSType.toString(string));
    }

    /**
     * ECMA 15.10.6.3 RegExp.prototype.test(string)
     *
     * @param self   self reference
     * @param string string to test for matches against regexp
     * @return true if matches found, false otherwise
     */
    @Function(attributes = Attribute.NOT_ENUMERABLE)
    public static Object test(final Object self, final Object string) {
        return checkRegExp(self).test(JSType.toString(string));
    }

    /**
     * ECMA 15.10.6.4 RegExp.prototype.toString()
     *
     * @param self self reference
     * @return string version of regexp
     */
    @Function(attributes = Attribute.NOT_ENUMERABLE)
    public static Object toString(final Object self) {
        return checkRegExp(self).toString();
    }

    /**
     * ECMA 15.10.7.1 source
     *
     * @param self self reference
     * @return the input string for the regexp
     */
    @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
    public static Object source(final Object self) {
        return checkRegExp(self).input;
    }

    /**
     * ECMA 15.10.7.2 global
     *
     * @param self self reference
     * @return true if this regexp is flagged global, false otherwise
     */
    @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
    public static Object global(final Object self) {
        return checkRegExp(self).global;
    }

    /**
     * ECMA 15.10.7.3 ignoreCase
     *
     * @param self self reference
     * @return true if this regexp if flagged to ignore case, false otherwise
     */
    @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
    public static Object ignoreCase(final Object self) {
        return checkRegExp(self).ignoreCase;
    }

    /**
     * ECMA 15.10.7.4 multiline
     *
     * @param self self reference
     * @return true if this regexp is flagged to be multiline, false otherwise
     */
    @Getter(attributes = Attribute.NON_ENUMERABLE_CONSTANT)
    public static Object multiline(final Object self) {
        return checkRegExp(self).multiline;
    }

    /**
     * Getter for non-standard RegExp.input property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "input")
    public static Object getLastInput(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getInput();
    }

    /**
     * Getter for non-standard RegExp.multiline property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "multiline")
    public static Object getLastMultiline(Object self) {
        return false; // doesn't ever seem to become true and isn't documented anyhwere
    }

    /**
     * Getter for non-standard RegExp.lastMatch property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "lastMatch")
    public static Object getLastMatch(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getGroup(0);
    }

    /**
     * Getter for non-standard RegExp.lastParen property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "lastParen")
    public static Object getLastParen(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getLastParen();
    }

    /**
     * Getter for non-standard RegExp.leftContext property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "leftContext")
    public static Object getLeftContext(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getInput().substring(0, match.getIndex());
    }

    /**
     * Getter for non-standard RegExp.rightContext property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "rightContext")
    public static Object getRightContext(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getInput().substring(match.getIndex() + match.length());
    }

    /**
     * Getter for non-standard RegExp.$1 property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$1")
    public static Object getGroup1(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getGroup(1);
    }

    /**
     * Getter for non-standard RegExp.$2 property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$2")
    public static Object getGroup2(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getGroup(2);
    }

    /**
     * Getter for non-standard RegExp.$3 property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$3")
    public static Object getGroup3(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getGroup(3);
    }

    /**
     * Getter for non-standard RegExp.$4 property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$4")
    public static Object getGroup4(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getGroup(4);
    }

    /**
     * Getter for non-standard RegExp.$5 property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$5")
    public static Object getGroup5(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getGroup(5);
    }

    /**
     * Getter for non-standard RegExp.$6 property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$6")
    public static Object getGroup6(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getGroup(6);
    }

    /**
     * Getter for non-standard RegExp.$7 property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$7")
    public static Object getGroup7(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getGroup(7);
    }

    /**
     * Getter for non-standard RegExp.$8 property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$8")
    public static Object getGroup8(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getGroup(8);
    }

    /**
     * Getter for non-standard RegExp.$9 property.
     * @param self self object
     * @return last regexp input
     */
    @Getter(where = Where.CONSTRUCTOR, attributes = Attribute.CONSTANT, name = "$9")
    public static Object getGroup9(Object self) {
        final RegExpMatch match = Global.instance().getLastRegExpMatch();
        return match == null ? "" : match.getGroup(9);
    }

    private RegExpMatch execInner(final String string) {
        if (this.pattern == null) {
            return null; // never matches or similar, e.g. a[]
        }

        final Matcher matcher = pattern.matcher(string);
        final int start = this.global ? getLastIndex() : 0;

        if (start < 0 || start > string.length()) {
            setLastIndex(0);
            return null;
        }

        if (!matcher.find(start)) {
            setLastIndex(0);
            return null;
        }

        if (global) {
            setLastIndex(matcher.end());
        }

        final RegExpMatch match = new RegExpMatch(string, matcher.start(), groups(matcher));
        globalObject.setLastRegExpMatch(match);
        return match;
    }

    /**
     * Convert java.util.regex.Matcher groups to JavaScript groups.
     * That is, replace null and groups that didn't match with undefined.
     */
    private Object[] groups(final Matcher matcher) {
        final int groupCount = matcher.groupCount();
        final Object[] groups = new Object[groupCount + 1];
        for (int i = 0, lastGroupStart = matcher.start(); i <= groupCount; i++) {
            final int groupStart = matcher.start(i);
            if (lastGroupStart > groupStart
                    || (groupsInNegativeLookahead != null && groupsInNegativeLookahead.isSet(i))) {
                // (1) ECMA 15.10.2.5 NOTE 3: need to clear Atom's captures each time Atom is repeated.
                // (2) ECMA 15.10.2.8 NOTE 3: Backreferences to captures in (?!Disjunction) from elsewhere
                // in the pattern always return undefined because the negative lookahead must fail.
                groups[i] = UNDEFINED;
                continue;
            }
            final String group = matcher.group(i);
            groups[i] = group == null ? UNDEFINED : group;
            lastGroupStart = groupStart;
        }
        return groups;
    }

    /**
     * Executes a search for a match within a string based on a regular
     * expression. It returns an array of information or null if no match is
     * found.
     *
     * @param string String to match.
     * @return NativeArray of matches, string or null.
     */
    public Object exec(final String string) {
        final RegExpMatch match = execInner(string);

        if (match == null) {
            return null;
        }

        return new NativeRegExpExecResult(match);
    }

    /**
     * Executes a search for a match within a string based on a regular
     * expression.
     *
     * @param string String to match.
     * @return True if a match is found.
     */
    public Object test(final String string) {
        return exec(string) != null;
    }

    /**
     * Searches and replaces the regular expression portion (match) with the
     * replaced text instead. For the "replacement text" parameter, you can use
     * the keywords $1 to $2 to replace the original text with values from
     * sub-patterns defined within the main pattern.
     *
     * @param string String to match.
     * @param replacement Replacement string.
     * @return String with substitutions.
     */
    Object replace(final String string, final String replacement, final ScriptFunction function) {
        final Matcher matcher = pattern.matcher(string);
        /*
         * $$ -> $
         * $& -> the matched substring
         * $` -> the portion of string that preceeds matched substring
         * $' -> the portion of string that follows the matched substring
         * $n -> the nth capture, where n is [1-9] and $n is NOT followed by a decimal digit
         * $nn -> the nnth capture, where nn is a two digit decimal number [01-99].
         */
        String replace = replacement;

        if (!global) {
            if (!matcher.find()) {
                return string;
            }

            final StringBuilder sb = new StringBuilder();
            if (function != null) {
                replace = callReplaceValue(function, matcher, string);
            }
            appendReplacement(matcher, string, replace, sb, 0);
            sb.append(string, matcher.end(), string.length());
            return sb.toString();
        }

        int end = 0; // a.k.a. lastAppendPosition
        setLastIndex(0);

        boolean found;
        try {
            found = matcher.find(end);
        } catch (final IndexOutOfBoundsException e) {
            found = false;
        }

        if (!found) {
            return string;
        }

        int previousLastIndex = 0;
        final StringBuilder sb = new StringBuilder();
        do {
            if (function != null) {
                replace = callReplaceValue(function, matcher, string);
            }
            appendReplacement(matcher, string, replace, sb, end);
            end = matcher.end();

            // ECMA 15.5.4.10 String.prototype.match(regexp)
            final int thisIndex = end;
            if (thisIndex == previousLastIndex) {
                setLastIndex(thisIndex + 1);
                previousLastIndex = thisIndex + 1;
            } else {
                previousLastIndex = thisIndex;
            }
        } while (matcher.find());

        sb.append(string, end, string.length());

        return sb.toString();
    }

    private void appendReplacement(final Matcher matcher, final String text, final String replacement, final StringBuilder sb, final int lastAppendPosition) {
        // Process substitution string to replace group references with groups
        int cursor = 0;
        final StringBuilder result = new StringBuilder();
        Object[] groups = null;

        while (cursor < replacement.length()) {
            char nextChar = replacement.charAt(cursor);
            if (nextChar == '$') {
                // Skip past $
                cursor++;
                nextChar = replacement.charAt(cursor);
                final int firstDigit = nextChar - '0';

                if (firstDigit >= 0 && firstDigit <= 9 && firstDigit <= matcher.groupCount()) {
                    // $0 is not supported, but $01 is. implementation-defined: if n>m, ignore second digit.
                    int refNum = firstDigit;
                    cursor++;
                    if (cursor < replacement.length() && firstDigit < matcher.groupCount()) {
                        final int secondDigit = replacement.charAt(cursor) - '0';
                        if ((secondDigit >= 0) && (secondDigit <= 9)) {
                            final int newRefNum = (firstDigit * 10) + secondDigit;
                            if (newRefNum <= matcher.groupCount() && newRefNum > 0) {
                                // $nn ($01-$99)
                                refNum = newRefNum;
                                cursor++;
                            }
                        }
                    }
                    if (refNum > 0) {
                        if (groups == null) {
                            groups = groups(matcher);
                        }
                        // Append group if matched.
                        if (groups[refNum] != UNDEFINED) {
                            result.append((String) groups[refNum]);
                        }
                    } else { // $0. ignore.
                        assert refNum == 0;
                        result.append("$0");
                    }
                } else if (nextChar == '$') {
                    result.append('$');
                    cursor++;
                } else if (nextChar == '&') {
                    result.append(matcher.group());
                    cursor++;
                } else if (nextChar == '`') {
                    result.append(text.substring(0, matcher.start()));
                    cursor++;
                } else if (nextChar == '\'') {
                    result.append(text.substring(matcher.end()));
                    cursor++;
                } else {
                    // unknown substitution or $n with n>m. skip.
                    result.append('$');
                }
            } else {
                result.append(nextChar);
                cursor++;
            }
        }
        // Append the intervening text
        sb.append(text, lastAppendPosition, matcher.start());
        // Append the match substitution
        sb.append(result);
    }

    private String callReplaceValue(final ScriptFunction function, final Matcher matcher, final String string) {
        final Object[] groups = groups(matcher);
        final Object[] args   = Arrays.copyOf(groups, groups.length + 2);

        args[groups.length]     = matcher.start();
        args[groups.length + 1] = string;

        final Object self = function.isStrict() ? UNDEFINED : Global.instance();

        return JSType.toString(ScriptRuntime.apply(function, self, args));
    }

    /**
     * Breaks up a string into an array of substrings based on a regular
     * expression or fixed string.
     *
     * @param string String to match.
     * @param limit  Split limit.
     * @return Array of substrings.
     */
    Object split(final String string, final long limit) {
        return split(this, string, limit);
    }

    private static Object split(final NativeRegExp regexp0, final String input, final long limit) {
        final List<Object> matches = new ArrayList<>();

        final NativeRegExp regexp = new NativeRegExp(regexp0);
        regexp.setGlobal(true);

        if (limit == 0L) {
            return new NativeArray();
        }

        RegExpMatch match;
        final int inputLength = input.length();
        int lastLength = -1;
        int lastLastIndex = 0;

        while ((match = regexp.execInner(input)) != null) {
            final int lastIndex = match.getIndex() + match.length();

            if (lastIndex > lastLastIndex) {
                matches.add(input.substring(lastLastIndex, match.getIndex()));
                if (match.getGroups().length > 1 && match.getIndex() < inputLength) {
                    matches.addAll(Arrays.asList(match.getGroups()).subList(1, match.getGroups().length));
                }

                lastLength = match.length();
                lastLastIndex = lastIndex;

                if (matches.size() >= limit) {
                    break;
                }
            }

            // bump the index to avoid infinite loop
            if (regexp.getLastIndex() == match.getIndex()) {
                regexp.setLastIndex(match.getIndex() + 1);
            }
        }

        if (matches.size() < limit) {
            // check special case if we need to append an empty string at the
            // end of the match
            // if the lastIndex was the entire string
            if (lastLastIndex == input.length()) {
                if (lastLength > 0 || regexp.test("") == Boolean.FALSE) {
                    matches.add("");
                }
            } else {
                matches.add(input.substring(lastLastIndex, inputLength));
            }
        }

        return new NativeArray(matches.toArray());
    }

    /**
     * Tests for a match in a string. It returns the index of the match, or -1
     * if not found.
     *
     * @param string String to match.
     * @return Index of match.
     */
    Object search(final String string) {
        final RegExpMatch match = execInner(string);

        if (match == null) {
            return -1;
        }

        return match.getIndex();
    }

    /**
     * Fast lastIndex getter
     * @return last index property as int
     */
    public int getLastIndex() {
        return JSType.toInt32(lastIndex);
    }

    /**
     * Fast lastIndex getter
     * @return last index property as boxed integer
     */
    public Object getLastIndexObject() {
        return lastIndex;
    }

    /**
     * Fast lastIndex setter
     * @param lastIndex lastIndex
     */
    public void setLastIndex(final int lastIndex) {
        this.lastIndex = JSType.toObject(lastIndex);
    }

    private void init() {
        // Keep reference to global object to support "static" properties of RegExp
        this.globalObject = Global.instance();
        this.setProto(globalObject.getRegExpPrototype());
    }

    private static NativeRegExp checkRegExp(final Object self) {
        Global.checkObjectCoercible(self);
        if (self instanceof NativeRegExp) {
            return (NativeRegExp)self;
        } else if (self != null && self == Global.instance().getRegExpPrototype()) {
            return Global.instance().DEFAULT_REGEXP;
        } else {
            typeError("not.a.regexp", ScriptRuntime.safeToString(self));
            return null;
        }
    }

    private String getInput() {
        return input;
    }

    private void setInput(final String input) {
        this.input = input;
    }

    boolean getGlobal() {
        return global;
    }

    private void setGlobal(final boolean global) {
        this.global = global;
    }

    private boolean getIgnoreCase() {
        return ignoreCase;
    }

    private void setIgnoreCase(final boolean ignoreCase) {
        this.ignoreCase = ignoreCase;
    }

    private boolean getMultiline() {
        return multiline;
    }

    private void setMultiline(final boolean multiline) {
        this.multiline = multiline;
    }

    private Pattern getPattern() {
        return pattern;
    }

    private void setPattern(final Pattern pattern) {
        this.pattern = pattern;
    }

    private BitVector getGroupsInNegativeLookahead() {
        return groupsInNegativeLookahead;
    }

    private void setGroupsInNegativeLookahead(final BitVector groupsInNegativeLookahead) {
        this.groupsInNegativeLookahead = groupsInNegativeLookahead;
    }

}