NativeArray.java

/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.javascript;

import static org.mozilla.javascript.ArrayLikeAbstractOperations.getRawElem;

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
import org.mozilla.javascript.ArrayLikeAbstractOperations.IterativeOperation;
import org.mozilla.javascript.ArrayLikeAbstractOperations.ReduceOperation;
import org.mozilla.javascript.xml.XMLObject;

/**
 * This class implements the Array native object.
 *
 * @author Norris Boyd
 * @author Mike McCabe
 */
public class NativeArray extends ScriptableObject implements List {
    private static final long serialVersionUID = 7331366857676127338L;

    /*
     * Optimization possibilities and open issues:
     * - Long vs. double schizophrenia.  I suspect it might be better
     * to use double throughout.
     *
     * - Functions that need a new Array call "new Array" in the
     * current scope rather than using a hardwired constructor;
     * "Array" could be redefined.  It turns out that js calls the
     * equivalent of "new Array" in the current scope, except that it
     * always gets at least an object back, even when Array == null.
     */

    static final long MAX_ARRAY_INDEX = 0xfffffffel;

    private static final Object ARRAY_TAG = "Array";
    private static final String CLASS_NAME = "Array";
    private static final Long NEGATIVE_ONE = Long.valueOf(-1);
    private static final String[] UNSCOPABLES = {
        "at",
        "copyWithin",
        "entries",
        "fill",
        "find",
        "findIndex",
        "findLast",
        "findLastIndex",
        "flat",
        "flatMap",
        "includes",
        "keys",
        "toReversed",
        "toSorted",
        "toSpliced",
        "values"
    };

    static void init(Context cx, Scriptable scope, boolean sealed) {
        LambdaConstructor ctor =
                new LambdaConstructor(scope, CLASS_NAME, 1, NativeArray::jsConstructor);

        var proto = new NativeArray(0);
        ctor.setPrototypeScriptable(proto);

        defineMethodOnConstructor(ctor, scope, "of", 0, NativeArray::js_of);
        defineMethodOnConstructor(ctor, scope, "from", 1, NativeArray::js_from);
        defineMethodOnConstructor(ctor, scope, "isArray", 1, NativeArray::js_isArrayMethod);

        // The following need to appear on the constructor for
        // historical reasons even thought they should no tbe there
        // according to the spec.

        exposeMethodOnConstructor(ctor, scope, "join", 1, NativeArray::js_join);
        exposeMethodOnConstructor(ctor, scope, "reverse", 0, NativeArray::js_reverse);
        exposeMethodOnConstructor(ctor, scope, "sort", 1, NativeArray::js_sort);
        exposeMethodOnConstructor(ctor, scope, "push", 1, NativeArray::js_push);
        exposeMethodOnConstructor(ctor, scope, "pop", 0, NativeArray::js_pop);
        exposeMethodOnConstructor(ctor, scope, "shift", 0, NativeArray::js_shift);
        exposeMethodOnConstructor(ctor, scope, "unshift", 1, NativeArray::js_unshift);
        exposeMethodOnConstructor(ctor, scope, "splice", 2, NativeArray::js_splice);
        exposeMethodOnConstructor(ctor, scope, "concat", 1, NativeArray::js_concat);
        exposeMethodOnConstructor(ctor, scope, "slice", 2, NativeArray::js_slice);
        exposeMethodOnConstructor(ctor, scope, "indexOf", 1, NativeArray::js_indexOf);
        exposeMethodOnConstructor(ctor, scope, "lastIndexOf", 1, NativeArray::js_lastIndexOf);
        exposeMethodOnConstructor(ctor, scope, "every", 1, NativeArray::js_every);
        exposeMethodOnConstructor(ctor, scope, "filter", 1, NativeArray::js_filter);
        exposeMethodOnConstructor(ctor, scope, "forEach", 1, NativeArray::js_forEach);
        exposeMethodOnConstructor(ctor, scope, "map", 1, NativeArray::js_map);
        exposeMethodOnConstructor(ctor, scope, "some", 1, NativeArray::js_some);
        exposeMethodOnConstructor(ctor, scope, "find", 1, NativeArray::js_find);
        exposeMethodOnConstructor(ctor, scope, "findIndex", 1, NativeArray::js_findIndex);
        exposeMethodOnConstructor(ctor, scope, "findLast", 1, NativeArray::js_findLast);
        exposeMethodOnConstructor(ctor, scope, "findLastIndex", 1, NativeArray::js_findLastIndex);
        exposeMethodOnConstructor(ctor, scope, "reduce", 1, NativeArray::js_reduce);
        exposeMethodOnConstructor(ctor, scope, "reduceRight", 1, NativeArray::js_reduceRight);

        defineMethodOnPrototype(ctor, scope, "toString", 0, NativeArray::js_toString);
        defineMethodOnPrototype(ctor, scope, "toLocaleString", 0, NativeArray::js_toLocaleString);
        defineMethodOnPrototype(ctor, scope, "toSource", 0, NativeArray::js_toSource);
        defineMethodOnPrototype(ctor, scope, "join", 1, NativeArray::js_join);
        defineMethodOnPrototype(ctor, scope, "reverse", 0, NativeArray::js_reverse);
        defineMethodOnPrototype(ctor, scope, "sort", 1, NativeArray::js_sort);
        defineMethodOnPrototype(ctor, scope, "push", 1, NativeArray::js_push);
        defineMethodOnPrototype(ctor, scope, "pop", 0, NativeArray::js_pop);
        defineMethodOnPrototype(ctor, scope, "shift", 0, NativeArray::js_shift);
        defineMethodOnPrototype(ctor, scope, "unshift", 1, NativeArray::js_unshift);
        defineMethodOnPrototype(ctor, scope, "splice", 2, NativeArray::js_splice);
        defineMethodOnPrototype(ctor, scope, "concat", 1, NativeArray::js_concat);
        defineMethodOnPrototype(ctor, scope, "slice", 2, NativeArray::js_slice);
        defineMethodOnPrototype(ctor, scope, "indexOf", 1, NativeArray::js_indexOf);
        defineMethodOnPrototype(ctor, scope, "lastIndexOf", 1, NativeArray::js_lastIndexOf);
        defineMethodOnPrototype(ctor, scope, "includes", 1, NativeArray::js_includes);
        defineMethodOnPrototype(ctor, scope, "fill", 1, NativeArray::js_fill);
        defineMethodOnPrototype(ctor, scope, "copyWithin", 2, NativeArray::js_copyWithin);
        defineMethodOnPrototype(ctor, scope, "at", 1, NativeArray::js_at);
        defineMethodOnPrototype(ctor, scope, "flat", 0, NativeArray::js_flat);
        defineMethodOnPrototype(ctor, scope, "flatMap", 1, NativeArray::js_flatMap);
        defineMethodOnPrototype(ctor, scope, "every", 1, NativeArray::js_every);
        defineMethodOnPrototype(ctor, scope, "filter", 1, NativeArray::js_filter);
        defineMethodOnPrototype(ctor, scope, "forEach", 1, NativeArray::js_forEach);
        defineMethodOnPrototype(ctor, scope, "map", 1, NativeArray::js_map);
        defineMethodOnPrototype(ctor, scope, "some", 1, NativeArray::js_some);
        defineMethodOnPrototype(ctor, scope, "find", 1, NativeArray::js_find);
        defineMethodOnPrototype(ctor, scope, "findIndex", 1, NativeArray::js_findIndex);
        defineMethodOnPrototype(ctor, scope, "findLast", 1, NativeArray::js_findLast);
        defineMethodOnPrototype(ctor, scope, "findLastIndex", 1, NativeArray::js_findLastIndex);
        defineMethodOnPrototype(ctor, scope, "reduce", 1, NativeArray::js_reduce);
        defineMethodOnPrototype(ctor, scope, "reduceRight", 1, NativeArray::js_reduceRight);
        defineMethodOnPrototype(ctor, scope, "keys", 0, NativeArray::js_keys);
        defineMethodOnPrototype(ctor, scope, "entries", 0, NativeArray::js_entries);
        defineMethodOnPrototype(ctor, scope, "values", 0, NativeArray::js_values);
        defineMethodOnPrototype(ctor, scope, "toReversed", 0, NativeArray::js_toReversed);
        defineMethodOnPrototype(ctor, scope, "toSorted", 1, NativeArray::js_toSorted);
        defineMethodOnPrototype(ctor, scope, "toSpliced", 2, NativeArray::js_toSpliced);
        defineMethodOnPrototype(ctor, scope, "with", 2, NativeArray::js_with);

        ctor.definePrototypeAlias("values", SymbolKey.ITERATOR, DONTENUM);
        ScriptRuntimeES6.addSymbolSpecies(cx, scope, ctor);
        ScriptRuntimeES6.addSymbolUnscopables(
                cx,
                scope,
                proto,
                new LazilyLoadedCtor(
                        proto, "", false, false, (c, s, sld) -> makeUnscopables(c, s)));

        ctor.setPrototypePropertyAttributes(PERMANENT | READONLY | DONTENUM);
        ScriptableObject.defineProperty(scope, CLASS_NAME, ctor, DONTENUM);
        if (sealed) {
            ctor.sealObject();
            ((NativeArray) ctor.getPrototypeProperty()).sealObject();
        }
    }

    private static void defineMethodOnConstructor(
            LambdaConstructor constructor,
            Scriptable scope,
            String name,
            int length,
            SerializableCallable target) {
        constructor.defineConstructorMethod(
                scope, name, length, null, target, DONTENUM, DONTENUM | READONLY);
    }

    private static void defineMethodOnPrototype(
            LambdaConstructor constructor,
            Scriptable scope,
            String name,
            int length,
            SerializableCallable target) {
        constructor.definePrototypeMethod(
                scope, name, length, null, target, DONTENUM, DONTENUM | READONLY);
    }

    private static void exposeMethodOnConstructor(
            LambdaConstructor constructor,
            Scriptable scope,
            String name,
            int length,
            SerializableCallable target) {
        constructor.defineConstructorMethod(
                scope,
                name,
                length,
                null,
                (cx, s, thisObj, args) -> {
                    var realThis = ScriptRuntime.toObject(cx, scope, args[0]);
                    var realArgs = Arrays.copyOfRange(args, 1, args.length);
                    return target.call(cx, s, realThis, realArgs);
                },
                DONTENUM,
                DONTENUM | READONLY);
    }

    static int getMaximumInitialCapacity() {
        return maximumInitialCapacity;
    }

    static void setMaximumInitialCapacity(int maximumInitialCapacity) {
        NativeArray.maximumInitialCapacity = maximumInitialCapacity;
    }

    public NativeArray(long lengthArg) {
        denseOnly = lengthArg <= maximumInitialCapacity;
        if (denseOnly) {
            int intLength = (int) lengthArg;
            if (intLength < DEFAULT_INITIAL_CAPACITY) intLength = DEFAULT_INITIAL_CAPACITY;
            dense = new Object[intLength];
            Arrays.fill(dense, Scriptable.NOT_FOUND);
        }
        length = lengthArg;
        createLengthProp();
    }

    public NativeArray(Object[] array) {
        denseOnly = true;
        dense = array;
        length = array.length;
        createLengthProp();
    }

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

    private static final int Id_length = 1, MAX_INSTANCE_ID = 1;

    @Override
    public void setPrototype(Scriptable p) {
        super.setPrototype(p);
        if (!(p instanceof NativeArray)) {
            setDenseOnly(false);
        }
    }

    private static Object makeUnscopables(Context cx, Scriptable scope) {
        NativeObject obj;

        obj = (NativeObject) cx.newObject(scope);

        ScriptableObject desc = ScriptableObject.buildDataDescriptor(obj, true, EMPTY);
        for (var k : UNSCOPABLES) {
            obj.defineOwnProperty(cx, k, desc);
        }
        obj.setPrototype(null); // unscopables don't have any prototype
        return obj;
    }

    @Override
    public Object get(int index, Scriptable start) {
        if (!denseOnly && isGetterOrSetter(null, index, false)) return super.get(index, start);
        if (dense != null && 0 <= index && index < dense.length) return dense[index];
        return super.get(index, start);
    }

    @Override
    public boolean has(int index, Scriptable start) {
        if (!denseOnly && isGetterOrSetter(null, index, false)) return super.has(index, start);
        if (dense != null && 0 <= index && index < dense.length) return dense[index] != NOT_FOUND;
        return super.has(index, start);
    }

    private static long toArrayIndex(Object id) {
        if (id instanceof String) {
            return toArrayIndex((String) id);
        } else if (id instanceof Number) {
            return toArrayIndex(((Number) id).doubleValue());
        }
        return -1;
    }

    // if id is an array index (ECMA 15.4.0), return the number,
    // otherwise return -1L
    private static long toArrayIndex(String id) {
        long index = toArrayIndex(ScriptRuntime.toNumber(id));
        // Assume that ScriptRuntime.toString(index) is the same
        // as java.lang.Long.toString(index) for long
        if (Long.toString(index).equals(id)) {
            return index;
        }
        return -1;
    }

    private static long toArrayIndex(double d) {
        if (!Double.isNaN(d)) {
            long index = ScriptRuntime.toUint32(d);
            if (index == d && index != 4294967295L) {
                return index;
            }
        }
        return -1;
    }

    private static int toDenseIndex(Object id) {
        long index = toArrayIndex(id);
        return 0 <= index && index < Integer.MAX_VALUE ? (int) index : -1;
    }

    @Override
    public void put(String id, Scriptable start, Object value) {
        super.put(id, start, value);
        if (start == this) {
            // If the object is sealed, super will throw exception
            long index = toArrayIndex(id);
            if (index >= length) {
                length = index + 1;
                modCount++;
                denseOnly = false;
            }
        }
    }

    private boolean ensureCapacity(int capacity) {
        if (capacity > dense.length) {
            if (capacity > MAX_PRE_GROW_SIZE) {
                denseOnly = false;
                return false;
            }
            capacity = Math.max(capacity, (int) (dense.length * GROW_FACTOR));
            Object[] newDense = new Object[capacity];
            System.arraycopy(dense, 0, newDense, 0, dense.length);
            Arrays.fill(newDense, dense.length, newDense.length, Scriptable.NOT_FOUND);
            dense = newDense;
        }
        return true;
    }

    @Override
    public void put(int index, Scriptable start, Object value) {
        if (start == this
                && !isSealed()
                && dense != null
                && 0 <= index
                && (denseOnly || !isGetterOrSetter(null, index, true))) {
            if (!isExtensible() && this.length <= index) {
                return;
            } else if (index < dense.length) {
                dense[index] = value;
                if (this.length <= index) {
                    this.length = (long) index + 1;
                    this.modCount++;
                }
                return;
            } else if (denseOnly
                    && index < dense.length * GROW_FACTOR
                    && ensureCapacity(index + 1)) {
                dense[index] = value;
                this.length = (long) index + 1;
                this.modCount++;
                return;
            } else {
                denseOnly = false;
            }
        }
        super.put(index, start, value);
        if (start == this && (lengthAttr & READONLY) == 0) {
            // only set the array length if given an array index (ECMA 15.4.0)
            if (this.length <= index) {
                // avoid overflowing index!
                this.length = (long) index + 1;
                this.modCount++;
            }
        }
    }

    @Override
    public void delete(int index) {
        if (dense != null
                && 0 <= index
                && index < dense.length
                && !isSealed()
                && (denseOnly || !isGetterOrSetter(null, index, true))) {
            dense[index] = NOT_FOUND;
        } else {
            super.delete(index);
        }
    }

    @Override
    public Object[] getIds(boolean nonEnumerable, boolean getSymbols) {
        Object[] superIds = super.getIds(nonEnumerable, getSymbols);
        if (dense == null) {
            return superIds;
        }
        int N = dense.length;
        long currentLength = length;
        if (N > currentLength) {
            N = (int) currentLength;
        }
        if (N == 0) {
            return superIds;
        }
        int superLength = superIds.length;
        Object[] ids = new Object[N + superLength];

        int presentCount = 0;
        for (int i = 0; i != N; ++i) {
            // Replace existing elements by their indexes
            if (dense[i] != NOT_FOUND) {
                ids[presentCount] = Integer.valueOf(i);
                ++presentCount;
            }
        }
        if (presentCount != N) {
            // dense contains deleted elems, need to shrink the result
            Object[] tmp = new Object[presentCount + superLength];
            System.arraycopy(ids, 0, tmp, 0, presentCount);
            ids = tmp;
        }
        System.arraycopy(superIds, 0, ids, presentCount, superLength);
        return ids;
    }

    public List<Integer> getIndexIds() {
        Object[] ids = getIds();
        List<Integer> indices = new ArrayList<>(ids.length);
        for (Object id : ids) {
            int int32Id = ScriptRuntime.toInt32(id);
            if (int32Id >= 0
                    && ScriptRuntime.toString(int32Id).equals(ScriptRuntime.toString(id))) {
                indices.add(Integer.valueOf(int32Id));
            }
        }
        return indices;
    }

    @Override
    public Object getDefaultValue(Class<?> hint) {
        if (hint == ScriptRuntime.NumberClass) {
            Context cx = Context.getContext();
            if (cx.getLanguageVersion() == Context.VERSION_1_2) return Long.valueOf(length);
        }
        return super.getDefaultValue(hint);
    }

    private ScriptableObject defaultIndexPropertyDescriptor(Object value) {
        Scriptable scope = getParentScope();
        if (scope == null) scope = this;
        ScriptableObject desc = new NativeObject();
        ScriptRuntime.setBuiltinProtoAndParent(desc, scope, TopLevel.Builtins.Object);
        desc.defineProperty("value", value, EMPTY);
        desc.defineProperty("writable", Boolean.TRUE, EMPTY);
        desc.defineProperty("enumerable", Boolean.TRUE, EMPTY);
        desc.defineProperty("configurable", Boolean.TRUE, EMPTY);
        return desc;
    }

    @Override
    public int getAttributes(int index) {
        if (dense != null && index >= 0 && index < dense.length && dense[index] != NOT_FOUND) {
            return EMPTY;
        }
        return super.getAttributes(index);
    }

    @Override
    protected ScriptableObject getOwnPropertyDescriptor(Context cx, Object id) {
        if (dense != null) {
            int index = toDenseIndex(id);
            if (0 <= index && index < dense.length && dense[index] != NOT_FOUND) {
                Object value = dense[index];
                return defaultIndexPropertyDescriptor(value);
            }
        }
        return super.getOwnPropertyDescriptor(cx, id);
    }

    @Override
    protected boolean defineOwnProperty(
            Context cx, Object id, ScriptableObject desc, boolean checkValid) {
        long index = toArrayIndex(id);
        if (index >= length) {
            length = index + 1;
            modCount++;
        }

        if (index != -1 && dense != null) {
            Object[] values = dense;
            dense = null;
            denseOnly = false;
            for (int i = 0; i < values.length; i++) {
                if (values[i] != NOT_FOUND) {
                    if (!isExtensible()) {
                        // Force creating a slot, before calling .put(...) on the next line, which
                        // would otherwise fail on a array on which preventExtensions() has been
                        // called
                        setAttributes(i, 0);
                    }
                    put(i, this, values[i]);
                }
            }
        }

        super.defineOwnProperty(cx, id, desc, checkValid);

        if ("length".equals(id)) {
            lengthAttr =
                    getAttributes("length"); // Update cached attributes value for length property
        }
        return true;
    }

    /** See ECMA 15.4.1,2 */
    static Scriptable jsConstructor(Context cx, Scriptable scope, Object[] args) {
        if (args.length == 0) return new NativeArray(0);

        // Only use 1 arg as first element for version 1.2; for
        // any other version (including 1.3) follow ECMA and use it as
        // a length.
        NativeArray res;
        if (cx.getLanguageVersion() == Context.VERSION_1_2) {
            res = new NativeArray(args);
        } else {
            Object arg0 = args[0];
            if (args.length > 1 || !(arg0 instanceof Number)) {
                res = new NativeArray(args);
            } else {
                long len = ScriptRuntime.toUint32(arg0);
                if (len != ((Number) arg0).doubleValue()) {
                    String msg = ScriptRuntime.getMessageById("msg.arraylength.bad");
                    throw ScriptRuntime.rangeError(msg);
                }
                res = new NativeArray(len);
            }
        }

        return res;
    }

    private void createLengthProp() {
        ScriptableObject.defineBuiltInProperty(
                this,
                "length",
                DONTENUM | PERMANENT,
                NativeArray::lengthGetter,
                NativeArray::lengthSetter,
                NativeArray::lengthAttrSetter,
                NativeArray::arraySetLength);
    }

    private static Object lengthGetter(NativeArray array, Scriptable start) {
        return ScriptRuntime.wrapNumber((double) array.length);
    }

    private static boolean lengthSetter(
            NativeArray builtIn,
            Object value,
            Scriptable owner,
            Scriptable start,
            boolean isThrow) {
        builtIn.setLength(value);
        return true;
    }

    private static void lengthAttrSetter(NativeArray builtIn, int attrs) {
        builtIn.lengthAttr = attrs;
    }

    protected static boolean arraySetLength(
            NativeArray builtIn,
            BuiltInSlot<NativeArray> current,
            Object id,
            ScriptableObject desc,
            boolean checkValid,
            Object key,
            int index) {
        // 10.2.4.2 Step 1.
        Object value = getProperty(desc, "value");

        if (value == NOT_FOUND) {
            return ScriptableObject.defineOrdinaryProperty(
                    builtIn, id, desc, checkValid, key, index);
        }

        // 10.2.4.2 Steps 2 - 6
        long newLength = checkLength(value);

        Object writable = getProperty(desc, "writable");
        // 10.2.4.2 9 is true by definition

        // 10.2.4.2 10-11
        if (newLength >= builtIn.length) {
            return ScriptableObject.defineOrdinaryProperty(
                    builtIn, id, desc, checkValid, key, index);
        }

        boolean currentWritable = ((current.getAttributes() & READONLY) == 0);
        if (!currentWritable) {
            throw ScriptRuntime.typeErrorById("msg.change.value.with.writable.false", id);
        }
        boolean newWritable = true;
        if (writable != NOT_FOUND) {
            newWritable = isTrue(writable);
            putProperty(desc, "writable", true);
        }

        // The standard set path that will be done by this call will
        // clear any elements as required.
        if (ScriptableObject.defineOrdinaryProperty(builtIn, id, desc, checkValid, key, index)) {
            var currentAttrs = current.getAttributes();
            var newAttrs = newWritable ? (currentAttrs & ~READONLY) : (currentAttrs | READONLY);
            current.setAttributes(newAttrs);
            return true;
        }
        return false;
    }

    private static Scriptable callConstructorOrCreateArray(
            Context cx, Scriptable scope, Scriptable arg, long length, boolean lengthAlways) {
        Scriptable result = null;

        if (arg instanceof Constructable) {
            try {
                final Object[] args =
                        (lengthAlways || (length > 0))
                                ? new Object[] {Long.valueOf(length)}
                                : ScriptRuntime.emptyArgs;
                result = ((Constructable) arg).construct(cx, scope, args);
            } catch (EcmaError ee) {
                if (!"TypeError".equals(ee.getName())) {
                    throw ee;
                }
                // If we get here then it is likely that the function we called is not really
                // a constructor. Unfortunately there's no better way to tell in Rhino right now.
            }
        }

        if (result == null) {
            // "length" below is really a hint so don't worry if it's really large
            result = cx.newArray(scope, (length > Integer.MAX_VALUE) ? 0 : (int) length);
        }

        return result;
    }

    private static Object js_from(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        final Scriptable items =
                ScriptRuntime.toObject(scope, (args.length >= 1) ? args[0] : Undefined.instance);
        Object mapArg = (args.length >= 2) ? args[1] : Undefined.instance;
        Scriptable thisArg = Undefined.SCRIPTABLE_UNDEFINED;
        final boolean mapping = !Undefined.isUndefined(mapArg);
        Function mapFn = null;

        if (mapping) {
            if (!(mapArg instanceof Function)) {
                throw ScriptRuntime.typeErrorById("msg.map.function.not");
            }
            mapFn = (Function) mapArg;
            if (args.length >= 3) {
                thisArg = ensureScriptable(args[2]);
            }
        }

        Object iteratorProp = ScriptableObject.getProperty(items, SymbolKey.ITERATOR);
        if (!(items instanceof NativeArray)
                && (iteratorProp != Scriptable.NOT_FOUND)
                && !Undefined.isUndefined(iteratorProp)) {
            final Object iterator = ScriptRuntime.callIterator(items, cx, scope);
            if (!Undefined.isUndefined(iterator)) {
                final Scriptable result =
                        callConstructorOrCreateArray(cx, scope, thisObj, 0, false);
                long k = 0;
                try (IteratorLikeIterable it = new IteratorLikeIterable(cx, scope, iterator)) {
                    for (Object temp : it) {
                        if (mapping) {
                            temp =
                                    mapFn.call(
                                            cx,
                                            scope,
                                            thisArg,
                                            new Object[] {temp, Long.valueOf(k)});
                        }
                        ArrayLikeAbstractOperations.defineElem(cx, result, k, temp);
                        k++;
                    }
                }
                setLengthProperty(cx, result, k);
                return result;
            }
        }

        final long length = getLengthProperty(cx, items);
        final Scriptable result = callConstructorOrCreateArray(cx, scope, thisObj, length, true);
        for (long k = 0; k < length; k++) {
            Object temp = getElem(cx, items, k);
            if (mapping) {
                temp = mapFn.call(cx, scope, thisArg, new Object[] {temp, Long.valueOf(k)});
            }
            ArrayLikeAbstractOperations.defineElem(cx, result, k, temp);
        }

        setLengthProperty(cx, result, length);
        return result;
    }

    private static Object js_of(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        final Scriptable result =
                callConstructorOrCreateArray(cx, scope, thisObj, args.length, true);

        if (cx.getLanguageVersion() >= Context.VERSION_ES6 && result instanceof ScriptableObject) {
            ScriptableObject desc = ScriptableObject.buildDataDescriptor(result, null, EMPTY);
            for (int i = 0; i < args.length; i++) {
                desc.put("value", desc, args[i]);
                ((ScriptableObject) result).defineOwnProperty(cx, i, desc);
            }
        } else {
            for (int i = 0; i < args.length; i++) {
                ArrayLikeAbstractOperations.defineElem(cx, result, i, args[i]);
            }
        }
        setLengthProperty(cx, result, args.length);

        return result;
    }

    public long getLength() {
        return length;
    }

    /**
     * @deprecated Use {@link #getLength()} instead.
     */
    @Deprecated
    public long jsGet_length() {
        return getLength();
    }

    /**
     * Change the value of the internal flag that determines whether all storage is handed by a
     * dense backing array rather than an associative store.
     *
     * @param denseOnly new value for denseOnly flag
     * @throws IllegalArgumentException if an attempt is made to enable denseOnly after it was
     *     disabled; NativeArray code is not written to handle switching back to a dense
     *     representation
     */
    void setDenseOnly(boolean denseOnly) {
        if (denseOnly && !this.denseOnly) throw new IllegalArgumentException();
        this.denseOnly = denseOnly;
    }

    boolean getDenseOnly() {
        return denseOnly;
    }

    private boolean setLength(Object val) {
        /* XXX do we satisfy this?
         * 15.4.5.1 [[Put]](P, V):
         * 1. Call the [[CanPut]] method of A with name P.
         * 2. If Result(1) is false, return.
         * ?
         */
        double d = ScriptRuntime.toNumber(val);
        long longVal = ScriptRuntime.toUint32(val);

        if ((lengthAttr & READONLY) != 0) {
            return false;
        }

        if (longVal != d) {
            String msg = ScriptRuntime.getMessageById("msg.arraylength.bad");
            throw ScriptRuntime.rangeError(msg);
        }

        if (denseOnly) {
            if (longVal < length) {
                // downcast okay because denseOnly
                Arrays.fill(dense, (int) longVal, dense.length, NOT_FOUND);
                length = longVal;
                modCount++;
                return true;
            } else if (longVal < MAX_PRE_GROW_SIZE
                    && longVal < (length * GROW_FACTOR)
                    && ensureCapacity((int) longVal)) {
                length = longVal;
                modCount++;
                return true;
            } else {
                denseOnly = false;
            }
        }
        if (longVal < length) {
            // remove all properties between longVal and length
            if (length - longVal > 0x1000) {
                // assume that the representation is sparse
                Object[] e = getIds(); // will only find in object itself
                for (Object id : e) {
                    if (id instanceof String) {
                        // > MAXINT will appear as string
                        String strId = (String) id;
                        long index = toArrayIndex(strId);
                        if (index >= longVal) delete(strId);
                    } else {
                        int index = ((Integer) id).intValue();
                        if (index >= longVal) delete(index);
                    }
                }
            } else {
                // assume a dense representation
                for (long i = longVal; i < length; i++) {
                    deleteElem(this, i);
                }
            }
        }
        length = longVal;
        modCount++;
        return true;
    }

    private static long checkLength(Object val) {
        double d = ScriptRuntime.toNumber(val);
        long longVal = ScriptRuntime.toUint32(val);
        if (longVal != d) {
            String msg = ScriptRuntime.getMessageById("msg.arraylength.bad");
            throw ScriptRuntime.rangeError(msg);
        }
        return longVal;
    }

    /* Support for generic Array-ish objects.  Most of the Array
     * functions try to be generic; anything that has a length
     * property is assumed to be an array.
     * getLengthProperty returns 0 if obj does not have the length property
     * or its value is not convertible to a number.
     */
    static long getLengthProperty(Context cx, Scriptable obj) {
        // These will give numeric lengths within Uint32 range.
        if (obj instanceof NativeString) {
            return ((NativeString) obj).getLength();
        }
        if (obj instanceof NativeArray) {
            return ((NativeArray) obj).getLength();
        }
        if (obj instanceof XMLObject) {
            Callable lengthFunc = (Callable) obj.get("length", obj);
            return ((Number) lengthFunc.call(cx, obj, obj, ScriptRuntime.emptyArgs)).longValue();
        }

        Object len = ScriptableObject.getProperty(obj, "length");
        if (len == Scriptable.NOT_FOUND) {
            // toUint32(undefined) == 0
            return 0;
        }

        double doubleLen = ScriptRuntime.toNumber(len);

        // ToLength
        if (doubleLen > NativeNumber.MAX_SAFE_INTEGER) {
            return (long) NativeNumber.MAX_SAFE_INTEGER;
        }
        if (doubleLen < 0) {
            return 0;
        }
        return (long) doubleLen;
    }

    private static Object setLengthProperty(Context cx, Scriptable target, long length) {
        Object len = ScriptRuntime.wrapNumber((double) length);
        ScriptableObject.putProperty(target, "length", len);
        return len;
    }

    /* Utility functions to encapsulate index > Integer.MAX_VALUE
     * handling.  Also avoids unnecessary object creation that would
     * be necessary to use the general ScriptRuntime.get/setElem
     * functions... though this is probably premature optimization.
     */
    private static void deleteElem(Scriptable target, long index) {
        int i = (int) index;
        if (i == index) {
            target.delete(i);
        } else {
            target.delete(Long.toString(index));
        }
    }

    private static Object getElem(Context cx, Scriptable target, long index) {
        Object elem = getRawElem(target, index);
        return (elem != Scriptable.NOT_FOUND ? elem : Undefined.instance);
    }

    private static void defineElemOrThrow(Context cx, Scriptable target, long index, Object value) {
        if (index > NativeNumber.MAX_SAFE_INTEGER) {
            throw ScriptRuntime.typeErrorById("msg.arraylength.too.big", String.valueOf(index));
        } else {
            ArrayLikeAbstractOperations.defineElem(cx, target, index, value);
        }
    }

    private static void setElem(Context cx, Scriptable target, long index, Object value) {
        if (index > Integer.MAX_VALUE) {
            String id = Long.toString(index);
            ScriptableObject.putProperty(target, id, value);
        } else {
            ScriptableObject.putProperty(target, (int) index, value);
        }
    }

    // Similar as setElem(), but triggers deleteElem() if value is NOT_FOUND
    private static void setRawElem(Context cx, Scriptable target, long index, Object value) {
        if (value == NOT_FOUND) {
            deleteElem(target, index);
        } else {
            setElem(cx, target, index, value);
        }
    }

    private static String js_toString(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return toStringHelper(
                cx, scope, thisObj, cx.hasFeature(Context.FEATURE_TO_STRING_AS_SOURCE), false);
    }

    private static String js_toLocaleString(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return toStringHelper(cx, scope, thisObj, false, true);
    }

    private static String js_toSource(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return toStringHelper(cx, scope, thisObj, true, false);
    }

    private static String toStringHelper(
            Context cx, Scriptable scope, Scriptable thisObj, boolean toSource, boolean toLocale) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);

        /* It's probably redundant to handle long lengths in this
         * function; StringBuilders are limited to 2^31 in java.
         */
        long length = getLengthProperty(cx, o);

        StringBuilder result = new StringBuilder(256);

        // whether to return '4,unquoted,5' or '[4, "quoted", 5]'
        String separator;

        if (toSource) {
            result.append('[');
            separator = ", ";
        } else {
            separator = ",";
        }

        boolean haslast = false;
        long i = 0;

        boolean toplevel, iterating;
        if (cx.iterating == null) {
            toplevel = true;
            iterating = false;
            cx.iterating = new HashSet<Scriptable>();
        } else {
            toplevel = false;
            iterating = cx.iterating.contains(o);
        }

        // Make sure cx.iterating is set to null when done
        // so we don't leak memory
        try {
            if (!iterating) {
                // stop recursion
                cx.iterating.add(o);

                // make toSource print null and undefined values in recent versions
                boolean skipUndefinedAndNull =
                        !toSource || cx.getLanguageVersion() < Context.VERSION_1_5;
                for (i = 0; i < length; i++) {
                    if (i > 0) result.append(separator);
                    Object elem = getRawElem(o, i);
                    if (elem == NOT_FOUND
                            || (skipUndefinedAndNull
                                    && (elem == null || elem == Undefined.instance))) {
                        haslast = false;
                        continue;
                    }
                    haslast = true;

                    if (toSource) {
                        result.append(ScriptRuntime.uneval(cx, scope, elem));

                    } else if (elem instanceof String) {
                        result.append((String) elem);

                    } else {
                        if (toLocale) {
                            var fun =
                                    ScriptRuntime.getPropAndThis(elem, "toLocaleString", cx, scope);
                            elem = fun.call(cx, scope, ScriptRuntime.emptyArgs);
                        }
                        result.append(ScriptRuntime.toString(elem));
                    }
                }

                // processing of thisObj done, remove it from the recursion detector
                // to allow thisObj to be again in the array later on
                cx.iterating.remove(o);
            }
        } finally {
            if (toplevel) {
                cx.iterating = null;
            }
        }

        if (toSource) {
            // for [,,].length behavior; we want toString to be symmetric.
            if (!haslast && i > 0) result.append(", ]");
            else result.append(']');
        }
        return result.toString();
    }

    /** See ECMA 15.4.4.3 */
    private static String js_join(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);

        long llength = getLengthProperty(cx, o);
        int length = (int) llength;
        if (llength != length) {
            throw Context.reportRuntimeErrorById(
                    "msg.arraylength.too.big", String.valueOf(llength));
        }
        // if no args, use "," as separator
        String separator =
                (args.length < 1 || args[0] == Undefined.instance)
                        ? ","
                        : ScriptRuntime.toString(args[0]);
        if (o instanceof NativeArray) {
            NativeArray na = (NativeArray) o;
            if (na.denseOnly) {
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < length; i++) {
                    if (i != 0) {
                        sb.append(separator);
                    }
                    if (i < na.dense.length) {
                        Object temp = na.dense[i];
                        if (temp != null
                                && temp != Undefined.instance
                                && temp != Scriptable.NOT_FOUND) {
                            sb.append(ScriptRuntime.toString(temp));
                        }
                    }
                }
                return sb.toString();
            }
        }
        if (length == 0) {
            return "";
        }
        String[] buf = new String[length];
        int total_size = 0;
        for (int i = 0; i != length; i++) {
            Object temp = getElem(cx, o, i);
            if (temp != null && temp != Undefined.instance) {
                String str = ScriptRuntime.toString(temp);
                total_size += str.length();
                buf[i] = str;
            }
        }
        total_size += (length - 1) * separator.length();
        StringBuilder sb = new StringBuilder(total_size);
        for (int i = 0; i != length; i++) {
            if (i != 0) {
                sb.append(separator);
            }
            String str = buf[i];
            if (str != null) {
                // str == null for undefined or null
                sb.append(str);
            }
        }
        return sb.toString();
    }

    /** See ECMA 15.4.4.4 */
    private static Scriptable js_reverse(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);

        if (o instanceof NativeArray) {
            NativeArray na = (NativeArray) o;
            if (na.denseOnly) {
                for (int i = 0, j = ((int) na.length) - 1; i < j; i++, j--) {
                    Object temp = na.dense[i];
                    na.dense[i] = na.dense[j];
                    na.dense[j] = temp;
                }
                return o;
            }
        }
        long len = getLengthProperty(cx, o);

        long half = len / 2;
        for (long i = 0; i < half; i++) {
            long j = len - i - 1;
            Object temp1 = getRawElem(o, i);
            Object temp2 = getRawElem(o, j);
            setRawElem(cx, o, i, temp2);
            setRawElem(cx, o, j, temp1);
        }
        return o;
    }

    /** See ECMA 15.4.4.5 */
    private static Scriptable js_sort(
            final Context cx,
            final Scriptable scope,
            final Scriptable thisObj,
            final Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);
        Comparator<Object> comparator =
                ArrayLikeAbstractOperations.getSortComparator(cx, scope, args);
        return sort(cx, o, comparator);
    }

    private static Scriptable sort(Context cx, Scriptable o, Comparator<Object> comparator) {
        long llength = getLengthProperty(cx, o);
        final int length = (int) llength;
        if (llength != length) {
            throw Context.reportRuntimeErrorById(
                    "msg.arraylength.too.big", String.valueOf(llength));
        }
        // copy the JS array into a working array, so it can be
        // sorted cheaply.
        final Object[] working = new Object[length];
        for (int i = 0; i != length; ++i) {
            working[i] = getRawElem(o, i);
        }

        // Java's 'Arrays.sort' is guaranteed to be stable so we can use it; however,
        // if the comparator is not consistent, it throws an IllegalArgumentException.
        // In case where the comparator is not consistent, the ECMAScript specification states
        // that sort order is implementation-defined, so we can just return the original array.
        try {
            Arrays.sort(working, comparator);
        } catch (IllegalArgumentException e) {
            return o;
        }

        // copy the working array back into thisObj
        for (int i = 0; i < length; ++i) {
            setRawElem(cx, o, i, working[i]);
        }

        return o;
    }

    private static Object js_push(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);

        if (o instanceof NativeArray) {
            NativeArray na = (NativeArray) o;
            if (na.denseOnly && na.ensureCapacity((int) na.length + args.length)) {
                for (Object arg : args) {
                    na.dense[(int) na.length++] = arg;
                    na.modCount++;
                }
                return ScriptRuntime.wrapNumber((double) na.length);
            }
        }
        long length = getLengthProperty(cx, o);
        for (int i = 0; i < args.length; i++) {
            setElem(cx, o, length + i, args[i]);
        }

        length += args.length;
        Object lengthObj = setLengthProperty(cx, o, length);

        /*
         * If JS1.2, follow Perl4 by returning the last thing pushed.
         * Otherwise, return the new array length.
         */
        if (cx.getLanguageVersion() == Context.VERSION_1_2)
            // if JS1.2 && no arguments, return undefined.
            return args.length == 0 ? Undefined.instance : args[args.length - 1];

        return lengthObj;
    }

    private static Object js_pop(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);

        Object result;
        if (o instanceof NativeArray) {
            NativeArray na = (NativeArray) o;
            if (na.denseOnly && na.length > 0) {
                na.length--;
                na.modCount++;
                result = na.dense[(int) na.length];
                na.dense[(int) na.length] = NOT_FOUND;
                return result;
            }
        }
        long length = getLengthProperty(cx, o);
        if (length > 0) {
            length--;

            // Get the to-be-deleted property's value.
            result = getElem(cx, o, length);

            // We need to delete the last property, because 'thisObj' may not
            // have setLength which does that for us.
            deleteElem(o, length);
        } else {
            result = Undefined.instance;
        }
        // necessary to match js even when length < 0; js pop will give a
        // length property to any target it is called on.
        setLengthProperty(cx, o, length);

        return result;
    }

    private static Object js_shift(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);

        if (o instanceof NativeArray) {
            NativeArray na = (NativeArray) o;
            if (na.denseOnly && na.length > 0) {
                na.length--;
                na.modCount++;
                Object result = na.dense[0];
                System.arraycopy(na.dense, 1, na.dense, 0, (int) na.length);
                na.dense[(int) na.length] = NOT_FOUND;
                return result == NOT_FOUND ? Undefined.instance : result;
            }
        }
        Object result;
        long length = getLengthProperty(cx, o);
        if (length > 0) {
            long i = 0;
            length--;

            // Get the to-be-deleted property's value.
            result = getElem(cx, o, i);

            /*
             * Slide down the array above the first element.  Leave i
             * set to point to the last element.
             */
            if (length > 0) {
                for (i = 1; i <= length; i++) {
                    Object temp = getRawElem(o, i);
                    setRawElem(cx, o, i - 1, temp);
                }
            }
            // We need to delete the last property, because 'thisObj' may not
            // have setLength which does that for us.
            deleteElem(o, length);
        } else {
            result = Undefined.instance;
        }
        setLengthProperty(cx, o, length);
        return result;
    }

    private static Object js_unshift(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);

        if (o instanceof NativeArray) {
            NativeArray na = (NativeArray) o;
            if (na.denseOnly && na.ensureCapacity((int) na.length + args.length)) {
                System.arraycopy(na.dense, 0, na.dense, args.length, (int) na.length);
                System.arraycopy(args, 0, na.dense, 0, args.length);
                na.length += args.length;
                na.modCount++;
                return ScriptRuntime.wrapNumber((double) na.length);
            }
        }
        long length = getLengthProperty(cx, o);
        int argc = args.length;

        if (argc > 0) {
            if (length + argc > NativeNumber.MAX_SAFE_INTEGER) {
                throw ScriptRuntime.typeErrorById("msg.arraylength.too.big", length + argc);
            }

            /*  Slide up the array to make room for args at the bottom */
            if (length > 0) {
                for (long last = length - 1; last >= 0; last--) {
                    Object temp = getRawElem(o, last);
                    setRawElem(cx, o, last + argc, temp);
                }
            }

            /* Copy from argv to the bottom of the array. */
            for (int i = 0; i < args.length; i++) {
                setElem(cx, o, i, args[i]);
            }
        }
        /* Follow Perl by returning the new array length. */
        length += argc;
        return setLengthProperty(cx, o, length);
    }

    private static Object js_splice(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);

        NativeArray na = null;
        Object result = ArrayLikeAbstractOperations.arraySpeciesCreate(cx, scope, o, 0);
        NativeArray nar = null;
        boolean denseFrom = false;
        boolean denseRes = false;

        if (o instanceof NativeArray) {
            na = (NativeArray) o;
            denseFrom = na.denseOnly;
        }
        if (result instanceof NativeArray) {
            nar = (NativeArray) result;
            denseRes = nar.denseOnly;
        }

        /* create an empty Array to return. */
        scope = getTopLevelScope(scope);
        int argc = args.length;
        if (argc == 0) return cx.newArray(scope, 0);
        long length = getLengthProperty(cx, o);

        /* Convert the first argument into a starting index. */
        long begin =
                ArrayLikeAbstractOperations.toSliceIndex(ScriptRuntime.toInteger(args[0]), length);
        argc--;

        /* Convert the second argument into count */
        long actualDeleteCount;
        if (args.length == 1) {
            actualDeleteCount = length - begin;
        } else {
            double dcount = ScriptRuntime.toInteger(args[1]);
            if (dcount < 0) {
                actualDeleteCount = 0;
            } else if (dcount > (length - begin)) {
                actualDeleteCount = length - begin;
            } else {
                actualDeleteCount = (long) dcount;
            }
            argc--;
        }

        long end = begin + actualDeleteCount;
        long delta = argc - actualDeleteCount;

        if (length + delta > NativeNumber.MAX_SAFE_INTEGER) {
            throw ScriptRuntime.typeErrorById("msg.arraylength.too.big", length + delta);
        }
        if (actualDeleteCount > Integer.MAX_VALUE) {
            String msg = ScriptRuntime.getMessageById("msg.arraylength.bad");
            throw ScriptRuntime.rangeError(msg);
        }

        /* If there are elements to remove, put them into the return value. */
        if (actualDeleteCount != 0) {
            if (actualDeleteCount == 1 && (cx.getLanguageVersion() == Context.VERSION_1_2)) {
                /*
                 * JS lacks "list context", whereby in Perl one turns the
                 * single scalar that's spliced out into an array just by
                 * assigning it to @single instead of $single, or by using it
                 * as Perl push's first argument, for instance.
                 *
                 * JS1.2 emulated Perl too closely and returned a non-Array for
                 * the single-splice-out case, requiring callers to test and
                 * wrap in [] if necessary.  So JS1.3, default, and other
                 * versions all return an array of length 1 for uniformity.
                 */
                result = getElem(cx, o, begin);
            } else {
                if (denseFrom && denseRes) {
                    int intLen = (int) (end - begin);
                    Object[] copy = new Object[intLen];
                    System.arraycopy(na.dense, (int) begin, copy, 0, intLen);
                    nar.dense = copy;
                    nar.setLength(intLen);
                } else {
                    for (long last = begin; last != end; last++) {
                        Object temp = getRawElem(o, last);
                        if (temp != NOT_FOUND) {
                            ArrayLikeAbstractOperations.defineElem(
                                    cx, (ScriptableObject) result, last - begin, temp);
                        }
                    }
                    // Need to set length for sparse result array
                    setLengthProperty(cx, (ScriptableObject) result, end - begin);
                }
            }
        } else { // (actualDeleteCount == 0)
            if (cx.getLanguageVersion() == Context.VERSION_1_2) {
                /* Emulate C JS1.2; if no elements are removed, return undefined. */
                result = Undefined.instance;
            }
        }

        /* Find the direction (up or down) to copy and make way for argv. */
        if (denseFrom
                && length + delta < Integer.MAX_VALUE
                && na.ensureCapacity((int) (length + delta))) {
            System.arraycopy(
                    na.dense, (int) end, na.dense, (int) (begin + argc), (int) (length - end));
            if (argc > 0) {
                System.arraycopy(args, 2, na.dense, (int) begin, argc);
            }
            if (delta < 0) {
                Arrays.fill(na.dense, (int) (length + delta), (int) length, NOT_FOUND);
            }
            na.length = length + delta;
            na.modCount++;
            return result;
        }

        if (delta > 0) {
            for (long last = length - 1; last >= end; last--) {
                Object temp = getRawElem(o, last);
                setRawElem(cx, o, last + delta, temp);
            }
        } else if (delta < 0) {
            for (long last = end; last < length; last++) {
                Object temp = getRawElem(o, last);
                setRawElem(cx, o, last + delta, temp);
            }
            // Do this backwards because some implementations might use a
            // non-sparse array and therefore might not be able to handle
            // deleting elements "in the middle". This makes us compatible
            // with older Rhino releases.
            for (long k = length - 1; k >= length + delta; --k) {
                deleteElem(o, k);
            }
        }

        /* Copy from argv into the hole to complete the splice. */
        int argoffset = args.length - argc;
        for (int i = 0; i < argc; i++) {
            setElem(cx, o, begin + i, args[i + argoffset]);
        }

        /* Update length in case we deleted elements from the end. */
        setLengthProperty(cx, o, length + delta);
        return result;
    }

    private static boolean isConcatSpreadable(Context cx, Scriptable scope, Object val) {
        // First, look for the new @@isConcatSpreadable test as per ECMAScript 6 and up
        if (val instanceof Scriptable) {
            final Object spreadable =
                    ScriptableObject.getProperty((Scriptable) val, SymbolKey.IS_CONCAT_SPREADABLE);
            if ((spreadable != Scriptable.NOT_FOUND) && !Undefined.isUndefined(spreadable)) {
                // If @@isConcatSpreadable was undefined, we have to fall back to testing for an
                // array.
                // Otherwise, we found some value
                return ScriptRuntime.toBoolean(spreadable);
            }
        }

        if (cx.getLanguageVersion() < Context.VERSION_ES6) {
            // Otherwise, for older Rhino versions, fall back to the old algorithm, which treats
            // things with the Array constructor as arrays. However, this is contrary to ES6!
            final Constructable ctor = ScriptRuntime.getExistingCtor(cx, scope, "Array");
            if (ScriptRuntime.instanceOf(val, ctor, cx)) {
                return true;
            }
        }

        // Otherwise, it's only spreadable if it's a native array
        return js_isArray(val);
    }

    // Concat elements of "arg" into the destination, with optimizations for native,
    // dense arrays.
    private static long concatSpreadArg(
            Context cx, Scriptable result, Scriptable arg, long offset) {
        long srclen = getLengthProperty(cx, arg);
        long newlen = srclen + offset;

        if (newlen > NativeNumber.MAX_SAFE_INTEGER) {
            throw ScriptRuntime.typeErrorById("msg.arraylength.too.big", newlen);
        }

        // First, optimize for a pair of native, dense arrays
        if ((newlen <= Integer.MAX_VALUE) && (result instanceof NativeArray)) {
            final NativeArray denseResult = (NativeArray) result;
            if (denseResult.denseOnly && (arg instanceof NativeArray)) {
                final NativeArray denseArg = (NativeArray) arg;
                if (denseArg.denseOnly) {
                    // Now we can optimize
                    denseResult.ensureCapacity((int) newlen);
                    System.arraycopy(
                            denseArg.dense, 0, denseResult.dense, (int) offset, (int) srclen);
                    return newlen;
                }
                // We could also optimize here if we are copying to a dense target from a non-dense
                // native array. However, if the source array is very sparse then the result will be
                // very bad -- so don't.
            }
        }

        // If we get here then we have to do things the generic way
        long dstpos = offset;
        for (long srcpos = 0; srcpos < srclen; srcpos++, dstpos++) {
            final Object temp = getRawElem(arg, srcpos);
            if (temp != Scriptable.NOT_FOUND) {
                ArrayLikeAbstractOperations.defineElem(cx, result, dstpos, temp);
            }
        }
        return newlen;
    }

    private static long doConcat(
            Context cx, Scriptable scope, Scriptable result, Object arg, long offset) {
        if (isConcatSpreadable(cx, scope, arg)) {
            return concatSpreadArg(cx, result, (Scriptable) arg, offset);
        }
        ArrayLikeAbstractOperations.defineElem(cx, result, offset, arg);
        return offset + 1;
    }

    /*
     * See Ecma 262v3 15.4.4.4
     */
    private static Scriptable js_concat(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);

        // create an empty Array to return.
        scope = getTopLevelScope(scope);
        final Scriptable result = ArrayLikeAbstractOperations.arraySpeciesCreate(cx, scope, o, 0);

        long length = doConcat(cx, scope, result, o, 0);
        for (Object arg : args) {
            length = doConcat(cx, scope, result, arg, length);
        }

        setLengthProperty(cx, result, length);
        return result;
    }

    private static Scriptable js_slice(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);

        long len = getLengthProperty(cx, o);

        long begin, end;
        if (args.length == 0) {
            begin = 0;
            end = len;
        } else {
            begin = ArrayLikeAbstractOperations.toSliceIndex(ScriptRuntime.toInteger(args[0]), len);
            if (args.length == 1 || args[1] == Undefined.instance) {
                end = len;
            } else {
                end =
                        ArrayLikeAbstractOperations.toSliceIndex(
                                ScriptRuntime.toInteger(args[1]), len);
            }
        }

        if (end - begin > Integer.MAX_VALUE) {
            String msg = ScriptRuntime.getMessageById("msg.arraylength.bad");
            throw ScriptRuntime.rangeError(msg);
        }

        Scriptable result = ArrayLikeAbstractOperations.arraySpeciesCreate(cx, scope, o, 0);
        for (long slot = begin; slot < end; slot++) {
            Object temp = getRawElem(o, slot);
            if (temp != NOT_FOUND) {
                ArrayLikeAbstractOperations.defineElem(cx, result, slot - begin, temp);
            }
        }
        setLengthProperty(cx, result, Math.max(0, end - begin));

        return result;
    }

    private static Object js_indexOf(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Object compareTo = args.length > 0 ? args[0] : Undefined.instance;

        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);
        long length = getLengthProperty(cx, o);
        /*
         * From http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array:indexOf
         * The index at which to begin the search. Defaults to 0, i.e. the
         * whole array will be searched. If the index is greater than or
         * equal to the length of the array, -1 is returned, i.e. the array
         * will not be searched. If negative, it is taken as the offset from
         * the end of the array. Note that even when the index is negative,
         * the array is still searched from front to back. If the calculated
         * index is less than 0, the whole array will be searched.
         */
        long start;
        if (args.length < 2) {
            // default
            start = 0;
        } else {
            start = (long) ScriptRuntime.toInteger(args[1]);
            if (start < 0) {
                start += length;
                if (start < 0) start = 0;
            }
            if (start > length - 1) return NEGATIVE_ONE;
        }
        if (o instanceof NativeArray) {
            NativeArray na = (NativeArray) o;
            if (na.denseOnly) {
                Scriptable proto = na.getPrototype();
                for (int i = (int) start; i < length; i++) {
                    Object val = na.dense[i];
                    if (val == NOT_FOUND && proto != null) {
                        val = ScriptableObject.getProperty(proto, i);
                    }
                    if (val != NOT_FOUND && ScriptRuntime.shallowEq(val, compareTo)) {
                        return Long.valueOf(i);
                    }
                }
                return NEGATIVE_ONE;
            }
        }
        for (long i = start; i < length; i++) {
            Object val = getRawElem(o, i);
            if (val != NOT_FOUND && ScriptRuntime.shallowEq(val, compareTo)) {
                return Long.valueOf(i);
            }
        }
        return NEGATIVE_ONE;
    }

    private static Object js_lastIndexOf(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Object compareTo = args.length > 0 ? args[0] : Undefined.instance;

        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);
        long length = getLengthProperty(cx, o);
        /*
         * From http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array:lastIndexOf
         * The index at which to start searching backwards. Defaults to the
         * array's length, i.e. the whole array will be searched. If the
         * index is greater than or equal to the length of the array, the
         * whole array will be searched. If negative, it is taken as the
         * offset from the end of the array. Note that even when the index
         * is negative, the array is still searched from back to front. If
         * the calculated index is less than 0, -1 is returned, i.e. the
         * array will not be searched.
         */
        long start;
        if (args.length < 2) {
            // default
            start = length - 1;
        } else {
            start = (long) ScriptRuntime.toInteger(args[1]);
            if (start >= length) start = length - 1;
            else if (start < 0) start += length;
            if (start < 0) return NEGATIVE_ONE;
        }
        if (o instanceof NativeArray) {
            NativeArray na = (NativeArray) o;
            if (na.denseOnly) {
                Scriptable proto = na.getPrototype();
                for (int i = (int) start; i >= 0; i--) {
                    Object val = na.dense[i];
                    if (val == NOT_FOUND && proto != null) {
                        val = ScriptableObject.getProperty(proto, i);
                    }
                    if (val != NOT_FOUND && ScriptRuntime.shallowEq(val, compareTo)) {
                        return Long.valueOf(i);
                    }
                }
                return NEGATIVE_ONE;
            }
        }
        for (long i = start; i >= 0; i--) {
            Object val = getRawElem(o, i);
            if (val != NOT_FOUND && ScriptRuntime.shallowEq(val, compareTo)) {
                return Long.valueOf(i);
            }
        }
        return NEGATIVE_ONE;
    }

    /*
       See ECMA-262 22.1.3.13
    */
    private static Boolean js_includes(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {

        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);
        long len = getLengthProperty(cx, o);
        if (len == 0) return Boolean.FALSE;

        long k;
        if (args.length < 2) {
            k = 0;
        } else {
            k = (long) ScriptRuntime.toInteger(args[1]);
            if (k < 0) {
                k += len;
                if (k < 0) k = 0;
            }
            if (k > len - 1) return Boolean.FALSE;
        }

        Object compareTo = args.length > 0 ? args[0] : Undefined.instance;
        if (o instanceof NativeArray) {
            NativeArray na = (NativeArray) o;
            if (na.denseOnly) {
                Scriptable proto = na.getPrototype();
                for (int i = (int) k; i < len; i++) {
                    Object elementK = na.dense[i];
                    if (elementK == NOT_FOUND && proto != null) {
                        elementK = ScriptableObject.getProperty(proto, i);
                    }
                    if (elementK == NOT_FOUND) {
                        elementK = Undefined.instance;
                    }
                    if (ScriptRuntime.sameZero(elementK, compareTo)) {
                        return Boolean.TRUE;
                    }
                }
                return Boolean.FALSE;
            }
        }
        for (; k < len; k++) {
            Object elementK = getRawElem(o, k);
            if (elementK == NOT_FOUND) {
                elementK = Undefined.instance;
            }
            if (ScriptRuntime.sameZero(elementK, compareTo)) {
                return Boolean.TRUE;
            }
        }
        return Boolean.FALSE;
    }

    private static Object js_fill(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);
        long len = getLengthProperty(cx, o);

        long relativeStart = 0;
        if (args.length >= 2) {
            relativeStart = (long) ScriptRuntime.toInteger(args[1]);
        }
        final long k;
        if (relativeStart < 0) {
            k = Math.max((len + relativeStart), 0);
        } else {
            k = Math.min(relativeStart, len);
        }

        long relativeEnd = len;
        if (args.length >= 3 && !Undefined.isUndefined(args[2])) {
            relativeEnd = (long) ScriptRuntime.toInteger(args[2]);
        }
        final long fin;
        if (relativeEnd < 0) {
            fin = Math.max((len + relativeEnd), 0);
        } else {
            fin = Math.min(relativeEnd, len);
        }

        Object value = args.length > 0 ? args[0] : Undefined.instance;
        for (long i = k; i < fin; i++) {
            setRawElem(cx, thisObj, i, value);
        }

        return thisObj;
    }

    private static Object js_copyWithin(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);
        long len = getLengthProperty(cx, o);

        Object targetArg = (args.length >= 1) ? args[0] : Undefined.instance;
        long relativeTarget = (long) ScriptRuntime.toInteger(targetArg);
        long to;
        if (relativeTarget < 0) {
            to = Math.max((len + relativeTarget), 0);
        } else {
            to = Math.min(relativeTarget, len);
        }

        Object startArg = (args.length >= 2) ? args[1] : Undefined.instance;
        long relativeStart = (long) ScriptRuntime.toInteger(startArg);
        long from;
        if (relativeStart < 0) {
            from = Math.max((len + relativeStart), 0);
        } else {
            from = Math.min(relativeStart, len);
        }

        long relativeEnd = len;
        if (args.length >= 3 && !Undefined.isUndefined(args[2])) {
            relativeEnd = (long) ScriptRuntime.toInteger(args[2]);
        }
        final long fin;
        if (relativeEnd < 0) {
            fin = Math.max((len + relativeEnd), 0);
        } else {
            fin = Math.min(relativeEnd, len);
        }

        long count = Math.min(fin - from, len - to);
        int direction = 1;
        if (from < to && to < from + count) {
            direction = -1;
            from = from + count - 1;
            to = to + count - 1;
        }

        // Optimize for a native array. If properties were overridden with setters
        // and other non-default options then we won't get here.
        if ((o instanceof NativeArray) && (count <= Integer.MAX_VALUE)) {
            NativeArray na = (NativeArray) o;
            if (na.denseOnly) {
                for (; count > 0; count--) {
                    na.dense[(int) to] = na.dense[(int) from];
                    from += direction;
                    to += direction;
                }

                return thisObj;
            }
        }

        for (; count > 0; count--) {
            final Object temp = getRawElem(o, from);
            if ((temp == Scriptable.NOT_FOUND) || Undefined.isUndefined(temp)) {
                deleteElem(o, to);
            } else {
                setElem(cx, o, to, temp);
            }

            from += direction;
            to += direction;
        }

        return thisObj;
    }

    private static Object js_at(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);
        long len = getLengthProperty(cx, o);

        long relativeIndex = 0;
        if (args.length >= 1) {
            relativeIndex = (long) ScriptRuntime.toInteger(args[0]);
        }
        long k = (relativeIndex >= 0) ? relativeIndex : len + relativeIndex;
        if ((k < 0) || (k >= len)) {
            return Undefined.instance;
        }
        return getElem(cx, thisObj, k);
    }

    private static Object js_flat(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);
        double depth;
        if (args.length < 1 || Undefined.isUndefined(args[0])) {
            depth = 1;
        } else {
            depth = ScriptRuntime.toInteger(args[0]);
        }

        return flat(cx, scope, o, depth);
    }

    private static Scriptable flat(Context cx, Scriptable scope, Scriptable source, double depth) {
        long length = getLengthProperty(cx, source);

        Scriptable result;
        result = ArrayLikeAbstractOperations.arraySpeciesCreate(cx, scope, source, 0);
        long j = 0;
        for (long i = 0; i < length; i++) {
            Object elem = getRawElem(source, i);
            if (elem == Scriptable.NOT_FOUND) {
                continue;
            }
            if (depth >= 1 && js_isArray(elem)) {
                Scriptable arr = flat(cx, scope, (Scriptable) elem, depth - 1);
                long arrLength = getLengthProperty(cx, arr);
                for (long k = 0; k < arrLength; k++) {
                    Object temp = getRawElem(arr, k);
                    defineElemOrThrow(cx, result, j++, temp);
                }
            } else {
                defineElemOrThrow(cx, result, j++, elem);
            }
        }
        setLengthProperty(cx, result, j);
        return result;
    }

    private static Object js_flatMap(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);
        Object callbackArg = args.length > 0 ? args[0] : Undefined.instance;

        Function f = ArrayLikeAbstractOperations.getCallbackArg(cx, callbackArg);
        Scriptable parent = ScriptableObject.getTopLevelScope(f);
        Scriptable thisArg;
        if (args.length < 2 || args[1] == null || args[1] == Undefined.instance) {
            thisArg = parent;
        } else {
            thisArg = ScriptRuntime.toObject(cx, scope, args[1]);
        }

        long length = getLengthProperty(cx, o);

        Scriptable result = ArrayLikeAbstractOperations.arraySpeciesCreate(cx, scope, o, 0);
        long j = 0;
        for (long i = 0; i < length; i++) {
            Object elem = getRawElem(o, i);
            if (elem == Scriptable.NOT_FOUND) {
                continue;
            }
            Object[] innerArgs = new Object[] {elem, Long.valueOf(i), o};
            Object mapCall = f.call(cx, parent, thisArg, innerArgs);
            if (js_isArray(mapCall)) {
                Scriptable arr = (Scriptable) mapCall;
                long arrLength = getLengthProperty(cx, arr);
                for (long k = 0; k < arrLength; k++) {
                    Object temp = getRawElem(arr, k);
                    defineElemOrThrow(cx, result, j++, temp);
                }
            } else {
                defineElemOrThrow(cx, result, j++, mapCall);
            }
        }
        setLengthProperty(cx, result, j);
        return result;
    }

    private static Object js_every(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return ArrayLikeAbstractOperations.iterativeMethod(
                cx,
                ARRAY_TAG,
                "every",
                IterativeOperation.EVERY,
                scope,
                thisObj,
                args,
                NativeArray::getLengthProperty);
    }

    private static Object js_filter(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return ArrayLikeAbstractOperations.iterativeMethod(
                cx,
                ARRAY_TAG,
                "filter",
                IterativeOperation.FILTER,
                scope,
                thisObj,
                args,
                NativeArray::getLengthProperty);
    }

    private static Object js_forEach(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return ArrayLikeAbstractOperations.iterativeMethod(
                cx,
                ARRAY_TAG,
                "forEach",
                IterativeOperation.FOR_EACH,
                scope,
                thisObj,
                args,
                NativeArray::getLengthProperty);
    }

    private static Object js_map(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return ArrayLikeAbstractOperations.iterativeMethod(
                cx,
                ARRAY_TAG,
                "map",
                IterativeOperation.MAP,
                scope,
                thisObj,
                args,
                NativeArray::getLengthProperty);
    }

    private static Object js_some(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return ArrayLikeAbstractOperations.iterativeMethod(
                cx,
                ARRAY_TAG,
                "some",
                IterativeOperation.SOME,
                scope,
                thisObj,
                args,
                NativeArray::getLengthProperty);
    }

    private static Object js_find(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return ArrayLikeAbstractOperations.iterativeMethod(
                cx,
                ARRAY_TAG,
                "find",
                IterativeOperation.FIND,
                scope,
                thisObj,
                args,
                NativeArray::getLengthProperty);
    }

    private static Object js_findIndex(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return ArrayLikeAbstractOperations.iterativeMethod(
                cx,
                ARRAY_TAG,
                "findIndex",
                IterativeOperation.FIND_INDEX,
                scope,
                thisObj,
                args,
                NativeArray::getLengthProperty);
    }

    private static Object js_findLast(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return ArrayLikeAbstractOperations.iterativeMethod(
                cx,
                ARRAY_TAG,
                "findLast",
                IterativeOperation.FIND_LAST,
                scope,
                thisObj,
                args,
                NativeArray::getLengthProperty);
    }

    private static Object js_findLastIndex(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return ArrayLikeAbstractOperations.iterativeMethod(
                cx,
                ARRAY_TAG,
                "findLastIndex",
                IterativeOperation.FIND_LAST_INDEX,
                scope,
                thisObj,
                args,
                NativeArray::getLengthProperty);
    }

    private static Object js_reduce(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return ArrayLikeAbstractOperations.reduceMethod(
                cx, ReduceOperation.REDUCE, scope, thisObj, args);
    }

    private static Object js_reduceRight(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return ArrayLikeAbstractOperations.reduceMethod(
                cx, ReduceOperation.REDUCE_RIGHT, scope, thisObj, args);
    }

    private static Object js_keys(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        thisObj = ScriptRuntime.toObject(cx, scope, thisObj);
        return new NativeArrayIterator(
                scope, thisObj, NativeArrayIterator.ARRAY_ITERATOR_TYPE.KEYS);
    }

    private static Object js_entries(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        thisObj = ScriptRuntime.toObject(cx, scope, thisObj);
        return new NativeArrayIterator(
                scope, thisObj, NativeArrayIterator.ARRAY_ITERATOR_TYPE.ENTRIES);
    }

    private static Object js_values(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        thisObj = ScriptRuntime.toObject(cx, scope, thisObj);
        return new NativeArrayIterator(
                scope, thisObj, NativeArrayIterator.ARRAY_ITERATOR_TYPE.VALUES);
    }

    private static Object js_isArrayMethod(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        return Boolean.valueOf(args.length > 0 && js_isArray(args[0]));
    }

    private static boolean js_isArray(Object o) {
        if (!(o instanceof Scriptable)) {
            return false;
        }
        if (o instanceof NativeProxy) {
            return js_isArray(((NativeProxy) o).getTargetThrowIfRevoked());
        }
        return "Array".equals(((Scriptable) o).getClassName());
    }

    private static Object js_toSorted(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Comparator<Object> comparator =
                ArrayLikeAbstractOperations.getSortComparator(cx, scope, args);

        Scriptable source = ScriptRuntime.toObject(cx, scope, thisObj);
        long len = getLengthProperty(cx, source);

        if (len > Integer.MAX_VALUE) {
            String msg = ScriptRuntime.getMessageById("msg.arraylength.bad");
            throw ScriptRuntime.rangeError(msg);
        }
        Scriptable result = cx.newArray(scope, (int) len);

        for (int k = 0; k < len; ++k) {
            Object fromValue = getElem(cx, source, k);
            setElem(cx, result, k, fromValue);
        }

        sort(cx, result, comparator);
        return result;
    }

    private static Object js_toReversed(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable source = ScriptRuntime.toObject(cx, scope, thisObj);
        long len = getLengthProperty(cx, source);

        if (len > Integer.MAX_VALUE) {
            String msg = ScriptRuntime.getMessageById("msg.arraylength.bad");
            throw ScriptRuntime.rangeError(msg);
        }
        Scriptable result = cx.newArray(scope, (int) len);

        for (int k = 0; k < len; ++k) {
            int from = (int) len - k - 1;
            Object fromValue = getElem(cx, source, from);
            setElem(cx, result, k, fromValue);
        }

        return result;
    }

    private static Object js_toSpliced(
            Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable source = ScriptRuntime.toObject(cx, scope, thisObj);
        long len = getLengthProperty(cx, source);

        long actualStart = 0;
        if (args.length > 0) {
            actualStart =
                    ArrayLikeAbstractOperations.toSliceIndex(ScriptRuntime.toInteger(args[0]), len);
        }

        long insertCount = args.length > 2 ? args.length - 2 : 0;

        long actualSkipCount;
        if (args.length == 0) {
            actualSkipCount = 0;
        } else if (args.length == 1) {
            actualSkipCount = len - actualStart;
        } else {
            long sc = ScriptRuntime.toLength(args, 1);
            actualSkipCount = Math.max(0, Math.min(sc, len - actualStart));
        }

        long newLen = len + insertCount - actualSkipCount;
        if (newLen > NativeNumber.MAX_SAFE_INTEGER) {
            throw ScriptRuntime.typeErrorById("msg.arraylength.too.big", newLen);
        }
        if (newLen > Integer.MAX_VALUE) {
            String msg = ScriptRuntime.getMessageById("msg.arraylength.bad");
            throw ScriptRuntime.rangeError(msg);
        }

        Scriptable result = cx.newArray(scope, (int) newLen);

        long i = 0;
        long r = actualStart + actualSkipCount;

        while (i < actualStart) {
            Object e = getElem(cx, source, i);
            setElem(cx, result, i, e);
            i++;
        }

        for (int j = 2; j < args.length; j++) {
            setElem(cx, result, i, args[j]);
            i++;
        }

        while (i < newLen) {
            Object e = getElem(cx, source, r);
            setElem(cx, result, i, e);
            i++;
            r++;
        }

        return result;
    }

    private static Object js_with(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
        Scriptable source = ScriptRuntime.toObject(cx, scope, thisObj);

        long len = getLengthProperty(cx, source);
        long relativeIndex = args.length > 0 ? (int) ScriptRuntime.toInteger(args[0]) : 0;
        long actualIndex = relativeIndex >= 0 ? relativeIndex : len + relativeIndex;

        if (actualIndex < 0 || actualIndex >= len) {
            throw ScriptRuntime.rangeError("index out of range");
        }
        if (len > Integer.MAX_VALUE) {
            String msg = ScriptRuntime.getMessageById("msg.arraylength.bad");
            throw ScriptRuntime.rangeError(msg);
        }

        Scriptable result = cx.newArray(scope, (int) len);
        for (long k = 0; k < len; ++k) {
            Object value;
            if (k == actualIndex) {
                value = args.length > 1 ? args[1] : Undefined.instance;
            } else {
                value = getElem(cx, source, k);
            }
            setElem(cx, result, k, value);
        }

        return result;
    }

    // methods to implement java.util.List

    @Override
    public boolean contains(Object o) {
        return indexOf(o) > -1;
    }

    @Override
    public Object[] toArray() {
        return toArray(ScriptRuntime.emptyArgs);
    }

    @Override
    public Object[] toArray(Object[] a) {
        int len = size();
        Object[] array =
                a.length >= len
                        ? a
                        : (Object[])
                                java.lang.reflect.Array.newInstance(
                                        a.getClass().getComponentType(), len);
        for (int i = 0; i < len; i++) {
            array[i] = get(i);
        }
        return array;
    }

    @Override
    public boolean containsAll(Collection c) {
        for (Object aC : c) if (!contains(aC)) return false;
        return true;
    }

    @Override
    public int size() {
        long longLen = length;
        if (longLen > Integer.MAX_VALUE) {
            throw new IllegalStateException(
                    "list.length (" + length + ") exceeds Integer.MAX_VALUE");
        }
        return (int) longLen;
    }

    @Override
    public boolean isEmpty() {
        return length == 0;
    }

    public Object get(long index) {
        if (index < 0 || index >= length) {
            throw new IndexOutOfBoundsException("Index: " + index + ", length: " + length);
        }
        Object value = getRawElem(this, index);
        if (value == Scriptable.NOT_FOUND || value == Undefined.instance) {
            return null;
        } else if (value instanceof Wrapper) {
            return ((Wrapper) value).unwrap();
        } else {
            return value;
        }
    }

    @Override
    public Object get(int index) {
        return get((long) index);
    }

    @Override
    public int indexOf(Object o) {
        int len = size();
        if (o == null) {
            for (int i = 0; i < len; i++) {
                if (get(i) == null) {
                    return i;
                }
            }
        } else {
            for (int i = 0; i < len; i++) {
                if (o.equals(get(i))) {
                    return i;
                }
            }
        }
        return -1;
    }

    @Override
    public int lastIndexOf(Object o) {
        int len = size();
        if (o == null) {
            for (int i = len - 1; i >= 0; i--) {
                if (get(i) == null) {
                    return i;
                }
            }
        } else {
            for (int i = len - 1; i >= 0; i--) {
                if (o.equals(get(i))) {
                    return i;
                }
            }
        }
        return -1;
    }

    @Override
    public Iterator iterator() {
        return listIterator(0);
    }

    @Override
    public ListIterator listIterator() {
        return listIterator(0);
    }

    @Override
    public ListIterator listIterator(final int start) {
        final int len = size();

        if (start < 0 || start > len) {
            throw new IndexOutOfBoundsException("Index: " + start + ", length: " + len);
        }

        return new ListIterator() {

            int cursor = start;
            int modCount = NativeArray.this.modCount;

            @Override
            public boolean hasNext() {
                return cursor < len;
            }

            @Override
            public Object next() {
                checkModCount(modCount);
                if (cursor == len) {
                    throw new NoSuchElementException();
                }
                return get(cursor++);
            }

            @Override
            public boolean hasPrevious() {
                return cursor > 0;
            }

            @Override
            public Object previous() {
                checkModCount(modCount);
                if (cursor == 0) {
                    throw new NoSuchElementException();
                }
                return get(--cursor);
            }

            @Override
            public int nextIndex() {
                return cursor;
            }

            @Override
            public int previousIndex() {
                return cursor - 1;
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }

            @Override
            public void add(Object o) {
                throw new UnsupportedOperationException();
            }

            @Override
            public void set(Object o) {
                throw new UnsupportedOperationException();
            }
        };
    }

    @Override
    public boolean add(Object o) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean remove(Object o) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean addAll(Collection c) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean removeAll(Collection c) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean retainAll(Collection c) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void clear() {
        throw new UnsupportedOperationException();
    }

    @Override
    public void add(int index, Object element) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean addAll(int index, Collection c) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object set(int index, Object element) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object remove(int index) {
        throw new UnsupportedOperationException();
    }

    @Override
    public List subList(int fromIndex, int toIndex) {
        if (fromIndex < 0) throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
        if (toIndex > size()) throw new IndexOutOfBoundsException("toIndex = " + toIndex);
        if (fromIndex > toIndex)
            throw new IllegalArgumentException(
                    "fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")");

        return new AbstractList() {
            private int mc = NativeArray.this.modCount;

            @Override
            public Object get(int index) {
                checkModCount(mc);
                return NativeArray.this.get(index + fromIndex);
            }

            @Override
            public int size() {
                checkModCount(mc);
                return toIndex - fromIndex;
            }
        };
    }

    private void checkModCount(int modCount) {
        if (this.modCount != modCount) {
            throw new ConcurrentModificationException();
        }
    }

    /** Internal representation of the JavaScript array's length property. */
    private long length;

    /** Attributes of the array's length property */
    private int lengthAttr = DONTENUM | PERMANENT;

    /** modCount required for subList/iterators */
    private transient int modCount;

    /**
     * Fast storage for dense arrays. Sparse arrays will use the superclass's hashtable storage
     * scheme.
     */
    private Object[] dense;

    /** True if all numeric properties are stored in <code>dense</code>. */
    private boolean denseOnly;

    /** The maximum size of <code>dense</code> that will be allocated initially. */
    private static int maximumInitialCapacity = 10000;

    /** The default capacity for <code>dense</code>. */
    private static final int DEFAULT_INITIAL_CAPACITY = 10;

    /** The factor to grow <code>dense</code> by. */
    private static final double GROW_FACTOR = 1.5;

    private static final int MAX_PRE_GROW_SIZE = (int) (Integer.MAX_VALUE / GROW_FACTOR);
}