/// import { BLOCK_MAXSIZE } from "./rt/common"; import { COMPARATOR, SORT } from "./util/sort"; import { joinBooleanArray, joinIntegerArray, joinFloatArray, joinStringArray, joinReferenceArray } from "./util/string"; import { idof, isArray as builtin_isArray } from "./builtins"; import { E_INDEXOUTOFRANGE, E_INVALIDLENGTH, E_ILLEGALGENTYPE, E_EMPTYARRAY, E_HOLEYARRAY } from "./util/error"; /** Ensures that the given array has _at least_ the specified backing size. */ function ensureSize(array: usize, minSize: usize, alignLog2: u32): void { // depends on the fact that Arrays mimic ArrayBufferView var oldCapacity = changetype(array).byteLength; if (minSize > oldCapacity >>> alignLog2) { if (minSize > BLOCK_MAXSIZE >>> alignLog2) throw new RangeError(E_INVALIDLENGTH); let oldData = changetype(changetype(array).buffer); let newCapacity = minSize << alignLog2; let newData = __realloc(oldData, newCapacity); // keeps RC memory.fill(newData + oldCapacity, 0, newCapacity - oldCapacity); if (newData !== oldData) { // oldData has been free'd store(array, newData, offsetof("buffer")); store(array, newData, offsetof("dataStart")); } store(array, newCapacity, offsetof("byteLength")); } } export class Array { [key: number]: T; // Mimicking ArrayBufferView isn't strictly necessary here but is done to allow glue code // to work with typed and normal arrays interchangeably. Technically, normal arrays do not need // `dataStart` (equals `buffer`) and `byteLength` (equals computed `buffer.byteLength`), but the // block is 16 bytes anyway so it's fine to have a couple extra fields in there. private buffer: ArrayBuffer; private dataStart: usize; private byteLength: i32; // Also note that Array with non-nullable T must guard against uninitialized null values // whenever an element is accessed. Otherwise, the compiler wouldn't be able to guarantee // type-safety anymore. For lack of a better word, such an array is "holey". private length_: i32; static isArray(value: U): bool { return isReference() ? builtin_isArray(value) && value !== null : false; } static create(capacity: i32 = 0): Array { WARNING("'Array.create' is deprecated. Use 'new Array' instead, making sure initial elements are initialized."); var array = new Array(capacity); array.length = 0; return array; } constructor(length: i32 = 0) { if (length > BLOCK_MAXSIZE >>> alignof()) throw new RangeError(E_INVALIDLENGTH); var bufferSize = length << alignof(); var buffer = __alloc(bufferSize, idof()); memory.fill(buffer, 0, bufferSize); this.buffer = changetype(buffer); // retains this.dataStart = buffer; this.byteLength = bufferSize; this.length_ = length; } get length(): i32 { return this.length_; } set length(newLength: i32) { var oldLength = this.length_; if (isManaged()) { if (oldLength > newLength) { // release no longer used refs let dataStart = this.dataStart; let cur = dataStart + (newLength << alignof()); let end = dataStart + (oldLength << alignof()); do __release(load(cur)); while ((cur += sizeof()) < end); } else { ensureSize(changetype(this), newLength, alignof()); } } else { ensureSize(changetype(this), newLength, alignof()); } this.length_ = newLength; } every(fn: (value: T, index: i32, array: Array) => bool): bool { for (let index = 0, length = this.length_; index < min(length, this.length_); ++index) { if (!fn(load(this.dataStart + (index << alignof())), index, this)) return false; } return true; } findIndex(predicate: (value: T, index: i32, array: Array) => bool): i32 { for (let index = 0, length = this.length_; index < min(length, this.length_); ++index) { if (predicate(load(this.dataStart + (index << alignof())), index, this)) return index; } return -1; } @operator("[]") private __get(index: i32): T { if (index >= this.length_) throw new RangeError(E_INDEXOUTOFRANGE); var value = this.__uget(index); if (isReference()) { if (!isNullable()) { if (!changetype(value)) throw new Error(E_HOLEYARRAY); } } return value; } @unsafe @operator("{}") private __uget(index: i32): T { return load(this.dataStart + (index << alignof())); } @operator("[]=") private __set(index: i32, value: T): void { if (index >= this.length_) { if (index < 0) throw new RangeError(E_INDEXOUTOFRANGE); ensureSize(changetype(this), index + 1, alignof()); this.length_ = index + 1; } this.__uset(index, value); } @unsafe @operator("{}=") private __uset(index: i32, value: T): void { if (isManaged()) { let offset = this.dataStart + (index << alignof()); let oldRef = load(offset); if (changetype(value) != oldRef) { store(offset, __retain(changetype(value))); __release(oldRef); } } else { store(this.dataStart + (index << alignof()), value); } } fill(value: T, start: i32 = 0, end: i32 = i32.MAX_VALUE): this { var dataStart = this.dataStart; var length = this.length_; start = start < 0 ? max(length + start, 0) : min(start, length); end = end < 0 ? max(length + end, 0) : min(end, length); if (isManaged()) { for (; start < end; ++start) { let oldRef: usize = load(dataStart + (start << alignof())); if (changetype(value) != oldRef) { store(dataStart + (start << alignof()), __retain(changetype(value))); __release(oldRef); } } } else if (sizeof() == 1) { if (start < end) { memory.fill( dataStart + start, u8(value), (end - start) ); } } else { for (; start < end; ++start) { store(dataStart + (start << alignof()), value); } } return this; } includes(value: T, fromIndex: i32 = 0): bool { if (isFloat()) { let length = this.length_; if (length == 0 || fromIndex >= length) return false; if (fromIndex < 0) fromIndex = max(length + fromIndex, 0); let dataStart = this.dataStart; while (fromIndex < length) { let elem = load(dataStart + (fromIndex << alignof())); // @ts-ignore if (elem == value || isNaN(elem) & isNaN(value)) return true; ++fromIndex; } return false; } else { return this.indexOf(value, fromIndex) >= 0; } } indexOf(value: T, fromIndex: i32 = 0): i32 { var length = this.length_; if (length == 0 || fromIndex >= length) return -1; if (fromIndex < 0) fromIndex = max(length + fromIndex, 0); var dataStart = this.dataStart; while (fromIndex < length) { if (load(dataStart + (fromIndex << alignof())) == value) return fromIndex; ++fromIndex; } return -1; } lastIndexOf(value: T, fromIndex: i32 = this.length_): i32 { var length = this.length_; if (length == 0) return -1; if (fromIndex < 0) fromIndex = length + fromIndex; else if (fromIndex >= length) fromIndex = length - 1; var dataStart = this.dataStart; while (fromIndex >= 0) { if (load(dataStart + (fromIndex << alignof())) == value) return fromIndex; --fromIndex; } return -1; } push(value: T): i32 { var length = this.length_; var newLength = length + 1; ensureSize(changetype(this), newLength, alignof()); if (isManaged()) { store(this.dataStart + (length << alignof()), __retain(changetype(value))); } else { store(this.dataStart + (length << alignof()), value); } this.length_ = newLength; return newLength; } concat(other: Array): Array { var thisLen = this.length_; var otherLen = select(0, other.length_, other === null); var outLen = thisLen + otherLen; if (outLen > BLOCK_MAXSIZE >>> alignof()) throw new Error(E_INVALIDLENGTH); var out = changetype>(__allocArray(outLen, alignof(), idof>())); // retains var outStart = out.dataStart; var thisSize = thisLen << alignof(); if (isManaged()) { let thisStart = this.dataStart; for (let offset: usize = 0; offset < thisSize; offset += sizeof()) { let ref = load(thisStart + offset); store(outStart + offset, __retain(ref)); } outStart += thisSize; let otherStart = other.dataStart; let otherSize = otherLen << alignof(); for (let offset: usize = 0; offset < otherSize; offset += sizeof()) { let ref = load(otherStart + offset); store(outStart + offset, __retain(ref)); } } else { memory.copy(outStart, this.dataStart, thisSize); memory.copy(outStart + thisSize, other.dataStart, otherLen << alignof()); } return out; } copyWithin(target: i32, start: i32, end: i32 = i32.MAX_VALUE): this { var dataStart = this.dataStart; var len = this.length_; end = min(end, len); var to = target < 0 ? max(len + target, 0) : min(target, len); var from = start < 0 ? max(len + start, 0) : min(start, len); var last = end < 0 ? max(len + end, 0) : min(end, len); var count = min(last - from, len - to); if (isManaged()) { if (from < to && to < (from + count)) { // right to left from += count - 1; to += count - 1; while (count) { let oldRef: usize = load(dataStart + (to << alignof())); let newRef: usize = load(dataStart + (from << alignof())); if (newRef != oldRef) { store(dataStart + (to << alignof()), __retain(newRef)); __release(oldRef); } --from, --to, --count; } } else { // left to right while (count) { let oldRef: usize = load(dataStart + (to << alignof())); let newRef: usize = load(dataStart + (from << alignof())); if (newRef != oldRef) { store(dataStart + (to << alignof()), __retain(newRef)); __release(oldRef); } ++from, ++to, --count; } } } else { memory.copy( // is memmove dataStart + (to << alignof()), dataStart + (from << alignof()), count << alignof() ); } return this; } pop(): T { var length = this.length_; if (length < 1) throw new RangeError(E_EMPTYARRAY); var element = load(this.dataStart + ((--length) << alignof())); this.length_ = length; return element; // no need to retain -> is moved } forEach(fn: (value: T, index: i32, array: Array) => void): void { for (let index = 0, length = this.length_; index < min(length, this.length_); ++index) { fn(load(this.dataStart + (index << alignof())), index, this); } } map(fn: (value: T, index: i32, array: Array) => U): Array { var length = this.length_; var out = changetype>(__allocArray(length, alignof(), idof>())); // retains var outStart = out.dataStart; for (let index = 0; index < min(length, this.length_); ++index) { let result = fn(load(this.dataStart + (index << alignof())), index, this); // retains if (isManaged()) { store(outStart + (index << alignof()), __retain(changetype(result))); } else { store(outStart + (index << alignof()), result); } // releases result } return out; } filter(fn: (value: T, index: i32, array: Array) => bool): Array { var result = changetype>(__allocArray(0, alignof(), idof>())); // retains for (let index = 0, length = this.length_; index < min(length, this.length_); ++index) { let value = load(this.dataStart + (index << alignof())); if (fn(value, index, this)) result.push(value); } return result; } reduce( fn: (previousValue: U, currentValue: T, currentIndex: i32, array: Array) => U, initialValue: U ): U { var accum = initialValue; for (let index = 0, length = this.length_; index < min(length, this.length_); ++index) { accum = fn(accum, load(this.dataStart + (index << alignof())), index, this); } return accum; } reduceRight( fn: (previousValue: U, currentValue: T, currentIndex: i32, array: Array) => U, initialValue: U ): U { var accum = initialValue; for (let index = this.length_ - 1; index >= 0; --index) { accum = fn(accum, load(this.dataStart + (index << alignof())), index, this); } return accum; } shift(): T { var length = this.length_; if (length < 1) throw new RangeError(E_EMPTYARRAY); var base = this.dataStart; var element = load(base); var lastIndex = length - 1; memory.copy( base, base + sizeof(), lastIndex << alignof() ); if (isReference()) { store(base + (lastIndex << alignof()), 0); } else { // @ts-ignore store(base + (lastIndex << alignof()), 0); } this.length_ = lastIndex; return element; // no need to retain -> is moved } some(fn: (value: T, index: i32, array: Array) => bool): bool { for (let index = 0, length = this.length_; index < min(length, this.length_); ++index) { if (fn(load(this.dataStart + (index << alignof())), index, this)) return true; } return false; } unshift(value: T): i32 { var newLength = this.length_ + 1; ensureSize(changetype(this), newLength, alignof()); var dataStart = this.dataStart; memory.copy( dataStart + sizeof(), dataStart, (newLength - 1) << alignof() ); if (isManaged()) { store(dataStart, __retain(changetype(value))); } else { store(dataStart, value); } this.length_ = newLength; return newLength; } slice(start: i32 = 0, end: i32 = i32.MAX_VALUE): Array { var length = this.length_; start = start < 0 ? max(start + length, 0) : min(start, length); end = end < 0 ? max(end + length, 0) : min(end , length); length = max(end - start, 0); var slice = changetype>(__allocArray(length, alignof(), idof>())); // retains var sliceBase = slice.dataStart; var thisBase = this.dataStart + (start << alignof()); if (isManaged()) { let off = 0; let end = length << alignof(); while (off < end) { let ref = load(thisBase + off); store(sliceBase + off, __retain(ref)); off += sizeof(); } } else { memory.copy(sliceBase, thisBase, length << alignof()); } return slice; } splice(start: i32, deleteCount: i32 = i32.MAX_VALUE): Array { var length = this.length_; start = start < 0 ? max(length + start, 0) : min(start, length); deleteCount = max(min(deleteCount, length - start), 0); var result = changetype>(__allocArray(deleteCount, alignof(), idof>())); // retains var resultStart = result.dataStart; var thisStart = this.dataStart; var thisBase = thisStart + (start << alignof()); // no need to retain -> is moved memory.copy( resultStart, thisBase, deleteCount << alignof() ); var offset = start + deleteCount; if (length != offset) { memory.copy( thisBase, thisStart + (offset << alignof()), (length - offset) << alignof() ); } this.length_ = length - deleteCount; return result; } reverse(): Array { var length = this.length_; if (length) { let front = this.dataStart; let back = this.dataStart + ((length - 1) << alignof()); while (front < back) { let temp = load(front); store(front, load(back)); store(back, temp); front += sizeof(); back -= sizeof(); } } return this; } sort(comparator: (a: T, b: T) => i32 = COMPARATOR()): this { var length = this.length_; if (length <= 1) return this; var base = this.dataStart; if (length == 2) { let a: T = load(base, sizeof()); // a = arr[1] let b: T = load(base); // b = arr[0] if (comparator(a, b) < 0) { store(base, b, sizeof()); // arr[1] = b; store(base, a); // arr[0] = a; } return this; } SORT(base, length, comparator); return this; } join(separator: string = ","): string { var dataStart = this.dataStart; var length = this.length_; if (isBoolean()) return joinBooleanArray(dataStart, length, separator); if (isInteger()) return joinIntegerArray(dataStart, length, separator); if (isFloat()) return joinFloatArray(dataStart, length, separator); if (ASC_SHRINK_LEVEL < 1) { if (isString()) return joinStringArray(dataStart, length, separator); } // For rest objects and arrays use general join routine if (isReference()) return joinReferenceArray(dataStart, length, separator); ERROR("unspported element type"); return unreachable(); } flat(): T { if (!isArray()) { throw new TypeError(E_ILLEGALGENTYPE); } // Get the length and data start values var length = this.length_; var selfDataStart = this.dataStart; // calculate the end size with an initial pass var size = 0; for (let i = 0; i < length; i++) { let child = load(selfDataStart + (i << alignof())); size += child == 0 ? 0 : load(child, offsetof("length_")); } // calculate the byteLength of the resulting backing ArrayBuffer var byteLength = size << usize(alignof>()); var dataStart = __alloc(byteLength, idof()); // create the return value and initialize it var result = __alloc(offsetof(), idof()); store(result, size, offsetof("length_")); // byteLength, dataStart, and buffer are all readonly store(result, byteLength, offsetof("byteLength")); store(result, dataStart, offsetof("dataStart")); store(result, __retain(dataStart), offsetof("buffer")); // set the elements var resultOffset: usize = 0; for (let i = 0; i < length; i++) { // for each child let child = load(selfDataStart + (i << alignof())); // ignore null arrays if (child == 0) continue; // copy the underlying buffer data to the result buffer let childDataLength = load(child, offsetof("byteLength")); memory.copy( dataStart + resultOffset, load(child, offsetof("dataStart")), childDataLength ); // advance the result length resultOffset += childDataLength; } // if the `valueof` type is managed, we must call __retain() on each reference if (isManaged>()) { for (let i = 0; i < size; i++) { __retain(load(dataStart + (i << usize(alignof>())))); } } return changetype(result); } toString(): string { return this.join(); } // RT integration @unsafe private __visit_impl(cookie: u32): void { if (isManaged()) { let cur = this.dataStart; let end = cur + (this.length_ << alignof()); while (cur < end) { let val = load(cur); if (val) __visit(val, cookie); cur += sizeof(); } } __visit(changetype(this.buffer), cookie); } }