blob: cd2d7096d3dbec1355654e69e695f83b2a238825 [file] [log] [blame]
/**
* Dynamic array implementation.
*
* Copyright: Copyright (C) 1999-2022 by The D Language Foundation, All Rights Reserved
* Authors: $(LINK2 https://www.digitalmars.com, Walter Bright)
* License: $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
* Source: $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/root/array.d, root/_array.d)
* Documentation: https://dlang.org/phobos/dmd_root_array.html
* Coverage: https://codecov.io/gh/dlang/dmd/src/master/src/dmd/root/array.d
*/
module dmd.root.array;
import core.stdc.stdlib : _compare_fp_t;
import core.stdc.string;
import dmd.root.rmem;
import dmd.root.string;
// `qsort` is only `nothrow` since 2.081.0
private extern(C) void qsort(scope void* base, size_t nmemb, size_t size, _compare_fp_t compar) nothrow @nogc;
debug
{
debug = stomp; // flush out dangling pointer problems by stomping on unused memory
}
extern (C++) struct Array(T)
{
size_t length;
private:
T[] data;
enum SMALLARRAYCAP = 1;
T[SMALLARRAYCAP] smallarray; // inline storage for small arrays
public:
/*******************
* Params:
* dim = initial length of array
*/
this(size_t dim) pure nothrow
{
reserve(dim);
this.length = dim;
}
@disable this(this);
~this() pure nothrow
{
debug (stomp) memset(data.ptr, 0xFF, data.length);
if (data.ptr != &smallarray[0])
mem.xfree(data.ptr);
}
///returns elements comma separated in []
extern(D) const(char)[] toString() const
{
static const(char)[] toStringImpl(alias toStringFunc, Array)(Array* a, bool quoted = false)
{
const(char)[][] buf = (cast(const(char)[]*)mem.xcalloc((char[]).sizeof, a.length))[0 .. a.length];
size_t len = 2; // [ and ]
const seplen = quoted ? 3 : 1; // ',' or null terminator and optionally '"'
if (a.length == 0)
len += 1; // null terminator
else
{
foreach (u; 0 .. a.length)
{
buf[u] = toStringFunc(a.data[u]);
len += buf[u].length + seplen;
}
}
char[] str = (cast(char*)mem.xmalloc_noscan(len))[0..len];
str[0] = '[';
char* p = str.ptr + 1;
foreach (u; 0 .. a.length)
{
if (u)
*p++ = ',';
if (quoted)
*p++ = '"';
memcpy(p, buf[u].ptr, buf[u].length);
p += buf[u].length;
if (quoted)
*p++ = '"';
}
*p++ = ']';
*p = 0;
assert(p - str.ptr == str.length - 1); // null terminator
mem.xfree(buf.ptr);
return str[0 .. $-1];
}
static if (is(typeof(T.init.toString())))
{
return toStringImpl!(a => a.toString)(&this);
}
else static if (is(typeof(T.init.toDString())))
{
return toStringImpl!(a => a.toDString)(&this, true);
}
else
{
assert(0);
}
}
///ditto
const(char)* toChars() const
{
return toString.ptr;
}
ref Array push(T ptr) return pure nothrow
{
reserve(1);
data[length++] = ptr;
return this;
}
extern (D) ref Array pushSlice(T[] a) return pure nothrow
{
const oldLength = length;
setDim(oldLength + a.length);
memcpy(data.ptr + oldLength, a.ptr, a.length * T.sizeof);
return this;
}
ref Array append(typeof(this)* a) return pure nothrow
{
insert(length, a);
return this;
}
void reserve(size_t nentries) pure nothrow
{
//printf("Array::reserve: length = %d, data.length = %d, nentries = %d\n", (int)length, (int)data.length, (int)nentries);
// Cold path
void enlarge(size_t nentries)
{
pragma(inline, false); // never inline cold path
if (data.length == 0)
{
// Not properly initialized, someone memset it to zero
if (nentries <= SMALLARRAYCAP)
{
data = SMALLARRAYCAP ? smallarray[] : null;
}
else
{
auto p = cast(T*)mem.xmalloc(nentries * T.sizeof);
data = p[0 .. nentries];
}
}
else if (data.length == SMALLARRAYCAP)
{
const allocdim = length + nentries;
auto p = cast(T*)mem.xmalloc(allocdim * T.sizeof);
memcpy(p, smallarray.ptr, length * T.sizeof);
data = p[0 .. allocdim];
}
else
{
/* Increase size by 1.5x to avoid excessive memory fragmentation
*/
auto increment = length / 2;
if (nentries > increment) // if 1.5 is not enough
increment = nentries;
const allocdim = length + increment;
debug (stomp)
{
// always move using allocate-copy-stomp-free
auto p = cast(T*)mem.xmalloc(allocdim * T.sizeof);
memcpy(p, data.ptr, length * T.sizeof);
memset(data.ptr, 0xFF, data.length * T.sizeof);
mem.xfree(data.ptr);
}
else
auto p = cast(T*)mem.xrealloc(data.ptr, allocdim * T.sizeof);
data = p[0 .. allocdim];
}
debug (stomp)
{
if (length < data.length)
memset(data.ptr + length, 0xFF, (data.length - length) * T.sizeof);
}
else
{
if (mem.isGCEnabled)
if (length < data.length)
memset(data.ptr + length, 0xFF, (data.length - length) * T.sizeof);
}
}
if (data.length - length < nentries) // false means hot path
enlarge(nentries);
}
void remove(size_t i) pure nothrow @nogc
{
if (length - i - 1)
memmove(data.ptr + i, data.ptr + i + 1, (length - i - 1) * T.sizeof);
length--;
debug (stomp) memset(data.ptr + length, 0xFF, T.sizeof);
}
void insert(size_t index, typeof(this)* a) pure nothrow
{
if (a)
{
size_t d = a.length;
reserve(d);
if (length != index)
memmove(data.ptr + index + d, data.ptr + index, (length - index) * T.sizeof);
memcpy(data.ptr + index, a.data.ptr, d * T.sizeof);
length += d;
}
}
void insert(size_t index, T ptr) pure nothrow
{
reserve(1);
memmove(data.ptr + index + 1, data.ptr + index, (length - index) * T.sizeof);
data[index] = ptr;
length++;
}
void setDim(size_t newdim) pure nothrow
{
if (length < newdim)
{
reserve(newdim - length);
}
length = newdim;
}
size_t find(T ptr) const nothrow pure
{
foreach (i; 0 .. length)
if (data[i] is ptr)
return i;
return size_t.max;
}
bool contains(T ptr) const nothrow pure
{
return find(ptr) != size_t.max;
}
ref inout(T) opIndex(size_t i) inout nothrow pure
{
debug
// This is called so often the array bounds become expensive
return data[i];
else
return data.ptr[i];
}
inout(T)* tdata() inout pure nothrow @nogc @trusted
{
return data.ptr;
}
Array!T* copy() const pure nothrow
{
auto a = new Array!T();
a.setDim(length);
memcpy(a.data.ptr, data.ptr, length * T.sizeof);
return a;
}
void shift(T ptr) pure nothrow
{
reserve(1);
memmove(data.ptr + 1, data.ptr, length * T.sizeof);
data[0] = ptr;
length++;
}
void zero() nothrow pure @nogc
{
data[0 .. length] = T.init;
}
T pop() nothrow pure @nogc
{
debug (stomp)
{
assert(length);
auto result = data[length - 1];
remove(length - 1);
return result;
}
else
return data[--length];
}
extern (D) inout(T)[] opSlice() inout nothrow pure @nogc
{
return data[0 .. length];
}
extern (D) inout(T)[] opSlice(size_t a, size_t b) inout nothrow pure @nogc
{
assert(a <= b && b <= length);
return data[a .. b];
}
/**
* Sort the elements of an array
*
* This function relies on `qsort`.
*
* Params:
* pred = Predicate to use. Should be a function of type
* `int function(scope const T* e1, scope const T* e2) nothrow`.
* The return value of this function should follow the
* usual C rule: `e1 >= e2 ? (e1 > e2) : -1`.
* The function can have D linkage.
*
* Returns:
* A reference to this, for easy chaining.
*/
extern(D) ref typeof(this) sort (alias pred) () nothrow
{
if (this.length < 2)
return this;
qsort(this.data.ptr, this.length, T.sizeof, &arraySortWrapper!(T, pred));
return this;
}
/// Ditto, but use `opCmp` by default
extern(D) ref typeof(this) sort () () nothrow
if (is(typeof(this.data[0].opCmp(this.data[1])) : int))
{
return this.sort!(function (scope const(T)* pe1, scope const(T)* pe2) => pe1.opCmp(*pe2));
}
alias opDollar = length;
alias dim = length;
}
unittest
{
// Test for objects implementing toString()
static struct S
{
int s = -1;
string toString() const
{
return "S";
}
}
auto array = Array!S(4);
assert(array.toString() == "[S,S,S,S]");
array.setDim(0);
assert(array.toString() == "[]");
// Test for toDString()
auto strarray = Array!(const(char)*)(2);
strarray[0] = "hello";
strarray[1] = "world";
auto str = strarray.toString();
assert(str == `["hello","world"]`);
// Test presence of null terminator.
assert(str.ptr[str.length] == '\0');
}
unittest
{
auto array = Array!double(4);
array.shift(10);
array.push(20);
array[2] = 15;
assert(array[0] == 10);
assert(array.find(10) == 0);
assert(array.find(20) == 5);
assert(!array.contains(99));
array.remove(1);
assert(array.length == 5);
assert(array[1] == 15);
assert(array.pop() == 20);
assert(array.length == 4);
array.insert(1, 30);
assert(array[1] == 30);
assert(array[2] == 15);
}
unittest
{
auto arrayA = Array!int(0);
int[3] buf = [10, 15, 20];
arrayA.pushSlice(buf);
assert(arrayA[] == buf[]);
auto arrayPtr = arrayA.copy();
assert(arrayPtr);
assert((*arrayPtr)[] == arrayA[]);
assert(arrayPtr.tdata != arrayA.tdata);
arrayPtr.setDim(0);
int[2] buf2 = [100, 200];
arrayPtr.pushSlice(buf2);
arrayA.append(arrayPtr);
assert(arrayA[3..$] == buf2[]);
arrayA.insert(0, arrayPtr);
assert(arrayA[] == [100, 200, 10, 15, 20, 100, 200]);
arrayA.zero();
foreach(e; arrayA)
assert(e == 0);
}
/**
* Exposes the given root Array as a standard D array.
* Params:
* array = the array to expose.
* Returns:
* The given array exposed to a standard D array.
*/
@property inout(T)[] peekSlice(T)(inout(Array!T)* array) pure nothrow @nogc
{
return array ? (*array)[] : null;
}
/**
* Splits the array at $(D index) and expands it to make room for $(D length)
* elements by shifting everything past $(D index) to the right.
* Params:
* array = the array to split.
* index = the index to split the array from.
* length = the number of elements to make room for starting at $(D index).
*/
void split(T)(ref Array!T array, size_t index, size_t length) pure nothrow
{
if (length > 0)
{
auto previousDim = array.length;
array.setDim(array.length + length);
for (size_t i = previousDim; i > index;)
{
i--;
array[i + length] = array[i];
}
}
}
unittest
{
auto array = Array!int();
array.split(0, 0);
assert([] == array[]);
array.push(1).push(3);
array.split(1, 1);
array[1] = 2;
assert([1, 2, 3] == array[]);
array.split(2, 3);
array[2] = 8;
array[3] = 20;
array[4] = 4;
assert([1, 2, 8, 20, 4, 3] == array[]);
array.split(0, 0);
assert([1, 2, 8, 20, 4, 3] == array[]);
array.split(0, 1);
array[0] = 123;
assert([123, 1, 2, 8, 20, 4, 3] == array[]);
array.split(0, 3);
array[0] = 123;
array[1] = 421;
array[2] = 910;
assert([123, 421, 910, 123, 1, 2, 8, 20, 4, 3] == (&array).peekSlice());
}
/**
* Reverse an array in-place.
* Params:
* a = array
* Returns:
* reversed a[]
*/
T[] reverse(T)(T[] a) pure nothrow @nogc @safe
{
if (a.length > 1)
{
const mid = (a.length + 1) >> 1;
foreach (i; 0 .. mid)
{
T e = a[i];
a[i] = a[$ - 1 - i];
a[$ - 1 - i] = e;
}
}
return a;
}
unittest
{
int[] a1 = [];
assert(reverse(a1) == []);
int[] a2 = [2];
assert(reverse(a2) == [2]);
int[] a3 = [2,3];
assert(reverse(a3) == [3,2]);
int[] a4 = [2,3,4];
assert(reverse(a4) == [4,3,2]);
int[] a5 = [2,3,4,5];
assert(reverse(a5) == [5,4,3,2]);
}
unittest
{
//test toString/toChars. Identifier is a simple object that has a usable .toString
import dmd.identifier : Identifier;
import core.stdc.string : strcmp;
auto array = Array!Identifier();
array.push(new Identifier("id1"));
array.push(new Identifier("id2"));
string expected = "[id1,id2]";
assert(array.toString == expected);
assert(strcmp(array.toChars, expected.ptr) == 0);
}
/// Predicate to wrap a D function passed to `qsort`
private template arraySortWrapper(T, alias fn)
{
pragma(mangle, "arraySortWrapper_" ~ T.mangleof ~ "_" ~ fn.mangleof)
extern(C) int arraySortWrapper(scope const void* e1, scope const void* e2) nothrow
{
return fn(cast(const(T*))e1, cast(const(T*))e2);
}
}
// Test sorting
unittest
{
Array!(const(char)*) strings;
strings.push("World");
strings.push("Foo");
strings.push("baguette");
strings.push("Avocado");
strings.push("Hello");
// Newer frontend versions will work with (e1, e2) and infer the type
strings.sort!(function (scope const char** e1, scope const char** e2) => strcmp(*e1, *e2));
assert(strings[0] == "Avocado");
assert(strings[1] == "Foo");
assert(strings[2] == "Hello");
assert(strings[3] == "World");
assert(strings[4] == "baguette");
/// opCmp automatically supported
static struct MyStruct
{
int a;
int opCmp(const ref MyStruct other) const nothrow
{
// Reverse order
return other.a - this.a;
}
}
Array!MyStruct arr1;
arr1.push(MyStruct(2));
arr1.push(MyStruct(4));
arr1.push(MyStruct(256));
arr1.push(MyStruct(42));
arr1.sort();
assert(arr1[0].a == 256);
assert(arr1[1].a == 42);
assert(arr1[2].a == 4);
assert(arr1[3].a == 2);
/// But only if user defined
static struct OtherStruct
{
int a;
static int pred (scope const OtherStruct* pe1, scope const OtherStruct* pe2)
nothrow @nogc pure @safe
{
return pe1.a - pe2.a;
}
}
static assert (!is(typeof(Array!(OtherStruct).init.sort())));
static assert (!is(typeof(Array!(OtherStruct).init.sort!pred)));
}
/**
* Iterates the given array and calls the given callable for each element.
*
* Use this instead of `foreach` when the array may expand during iteration.
*
* Params:
* callable = the callable to call for each element
* array = the array to iterate
*
* See_Also: $(REF foreachDsymbol, dmd, dsymbol)
*/
template each(alias callable, T)
if (is(ReturnType!(typeof((T t) => callable(t))) == void))
{
void each(ref Array!T array)
{
// Do not use foreach, as the size of the array may expand during iteration
for (size_t i = 0; i < array.length; ++i)
callable(array[i]);
}
void each(Array!T* array)
{
if (array)
each!callable(*array);
}
}
///
@("iterate over an Array") unittest
{
static immutable expected = [2, 3, 4, 5];
Array!int array;
foreach (e ; expected)
array.push(e);
int[] result;
array.each!((e) {
result ~= e;
});
assert(result == expected);
}
@("iterate over a pointer to an Array") unittest
{
static immutable expected = [2, 3, 4, 5];
auto array = new Array!int;
foreach (e ; expected)
array.push(e);
int[] result;
array.each!((e) {
result ~= e;
});
assert(result == expected);
}
@("iterate while appending to the array being iterated") unittest
{
static immutable expected = [2, 3, 4, 5];
Array!int array;
foreach (e ; expected[0 .. $ - 1])
array.push(e);
int[] result;
array.each!((e) {
if (e == 2)
array.push(5);
result ~= e;
});
assert(array[] == expected);
assert(result == expected);
}
/**
* Iterates the given array and calls the given callable for each element.
*
* If `callable` returns `!= 0`, it will stop the iteration and return that
* value, otherwise it will return 0.
*
* Use this instead of `foreach` when the array may expand during iteration.
*
* Params:
* callable = the callable to call for each element
* array = the array to iterate
*
* Returns: the last value returned by `callable`
* See_Also: $(REF foreachDsymbol, dmd, dsymbol)
*/
template each(alias callable, T)
if (is(ReturnType!(typeof((T t) => callable(t))) == int))
{
int each(ref Array!T array)
{
// Do not use foreach, as the size of the array may expand during iteration
for (size_t i = 0; i < array.length; ++i)
{
if (const result = callable(array[i]))
return result;
}
return 0;
}
int each(Array!T* array)
{
return array ? each!callable(*array) : 0;
}
}
///
@("iterate over an Array and stop the iteration") unittest
{
Array!int array;
foreach (e ; [2, 3, 4, 5])
array.push(e);
int[] result;
const returnValue = array.each!((e) {
result ~= e;
if (e == 3)
return 8;
return 0;
});
assert(result == [2, 3]);
assert(returnValue == 8);
}
@("iterate over an Array") unittest
{
static immutable expected = [2, 3, 4, 5];
Array!int array;
foreach (e ; expected)
array.push(e);
int[] result;
const returnValue = array.each!((e) {
result ~= e;
return 0;
});
assert(result == expected);
assert(returnValue == 0);
}
@("iterate over a pointer to an Array and stop the iteration") unittest
{
auto array = new Array!int;
foreach (e ; [2, 3, 4, 5])
array.push(e);
int[] result;
const returnValue = array.each!((e) {
result ~= e;
if (e == 3)
return 9;
return 0;
});
assert(result == [2, 3]);
assert(returnValue == 9);
}
@("iterate while appending to the array being iterated and stop the iteration") unittest
{
Array!int array;
foreach (e ; [2, 3])
array.push(e);
int[] result;
const returnValue = array.each!((e) {
if (e == 2)
array.push(1);
result ~= e;
if (e == 1)
return 7;
return 0;
});
static immutable expected = [2, 3, 1];
assert(array[] == expected);
assert(result == expected);
assert(returnValue == 7);
}
/// Returns: A static array constructed from `array`.
pragma(inline, true) T[n] staticArray(T, size_t n)(auto ref T[n] array)
{
return array;
}
///
pure nothrow @safe @nogc unittest
{
enum a = [0, 1].staticArray;
static assert(is(typeof(a) == int[2]));
static assert(a == [0, 1]);
}
/// Returns: `true` if the two given ranges are equal
bool equal(Range1, Range2)(Range1 range1, Range2 range2)
{
template isArray(T)
{
static if (is(T U : U[]))
enum isArray = true;
else
enum isArray = false;
}
static if (isArray!Range1 && isArray!Range2 && is(typeof(range1 == range2)))
return range1 == range2;
else
{
static if (hasLength!Range1 && hasLength!Range2 && is(typeof(r1.length == r2.length)))
{
if (range1.length != range2.length)
return false;
}
for (; !range1.empty; range1.popFront(), range2.popFront())
{
if (range2.empty)
return false;
if (range1.front != range2.front)
return false;
}
return range2.empty;
}
}
///
pure nothrow @nogc @safe unittest
{
enum a = [ 1, 2, 4, 3 ].staticArray;
static assert(!equal(a[], a[1..$]));
static assert(equal(a[], a[]));
// different types
enum b = [ 1.0, 2, 4, 3].staticArray;
static assert(!equal(a[], b[1..$]));
static assert(equal(a[], b[]));
}
pure nothrow @safe unittest
{
static assert(equal([1, 2, 3].map!(x => x * 2), [1, 2, 3].map!(x => x * 2)));
static assert(!equal([1, 2].map!(x => x * 2), [1, 2, 3].map!(x => x * 2)));
}
/**
* Lazily filters the given range based on the given predicate.
*
* Returns: a range containing only elements for which the predicate returns
* `true`
*/
auto filter(alias predicate, Range)(Range range)
if (isInputRange!(Unqual!Range) && isPredicateOf!(predicate, ElementType!Range))
{
return Filter!(predicate, Range)(range);
}
///
pure nothrow @safe @nogc unittest
{
enum a = [1, 2, 3, 4].staticArray;
enum result = a[].filter!(e => e > 2);
enum expected = [3, 4].staticArray;
static assert(result.equal(expected[]));
}
private struct Filter(alias predicate, Range)
{
private Range range;
private bool primed;
private void prime()
{
if (primed)
return;
while (!range.empty && !predicate(range.front))
range.popFront();
primed = true;
}
@property bool empty()
{
prime();
return range.empty;
}
@property auto front()
{
assert(!range.empty);
prime();
return range.front;
}
void popFront()
{
assert(!range.empty);
prime();
do
{
range.popFront();
} while (!range.empty && !predicate(range.front));
}
auto opSlice()
{
return this;
}
}
/**
* Lazily iterates the given range and calls the given callable for each element.
*
* Returns: a range containing the result of each call to `callable`
*/
auto map(alias callable, Range)(Range range)
if (isInputRange!(Unqual!Range) && isCallableWith!(callable, ElementType!Range))
{
return Map!(callable, Range)(range);
}
///
pure nothrow @safe @nogc unittest
{
enum a = [1, 2, 3, 4].staticArray;
enum expected = [2, 4, 6, 8].staticArray;
enum result = a[].map!(e => e * 2);
static assert(result.equal(expected[]));
}
private struct Map(alias callable, Range)
{
private Range range;
@property bool empty()
{
return range.empty;
}
@property auto front()
{
assert(!range.empty);
return callable(range.front);
}
void popFront()
{
assert(!range.empty);
range.popFront();
}
static if (hasLength!Range)
{
@property auto length()
{
return range.length;
}
alias opDollar = length;
}
}
/// Returns: the length of the given range.
auto walkLength(Range)(Range range)
if (isInputRange!Range )
{
static if (hasLength!Range)
return range.length;
else
{
size_t result;
for (; !range.empty; range.popFront())
++result;
return result;
}
}
///
pure nothrow @safe @nogc unittest
{
enum a = [1, 2, 3, 4].staticArray;
static assert(a[].walkLength == 4);
enum c = a[].filter!(e => e > 2);
static assert(c.walkLength == 2);
}
/// Evaluates to the element type of `R`.
template ElementType(R)
{
static if (is(typeof(R.init.front.init) T))
alias ElementType = T;
else
alias ElementType = void;
}
/// Evaluates to `true` if the given type satisfy the input range interface.
enum isInputRange(R) =
is(typeof(R.init) == R)
&& is(ReturnType!(typeof((R r) => r.empty)) == bool)
&& is(typeof((return ref R r) => r.front))
&& !is(ReturnType!(typeof((R r) => r.front)) == void)
&& is(typeof((R r) => r.popFront));
/// Evaluates to `true` if `func` can be called with a value of `T` and returns
/// a value that is convertible to `bool`.
enum isPredicateOf(alias func, T) = is(typeof((T t) => !func(t)));
/// Evaluates to `true` if `func` be called withl a value of `T`.
enum isCallableWith(alias func, T) =
__traits(compiles, { auto _ = (T t) => func(t); });
private:
template ReturnType(T)
{
static if (is(T R == return))
alias ReturnType = R;
else
static assert(false, "argument is not a function");
}
alias Unqual(T) = ReturnType!(typeof((T t) => cast() t));
template hasLength(Range)
{
static if (is(typeof(((Range* r) => r.length)(null)) Length))
enum hasLength = is(Length == size_t);
else
enum hasLength = false;
}
/// Implements the range interface primitive `front` for built-in arrays.
@property ref inout(T) front(T)(return scope inout(T)[] a) pure nothrow @nogc @safe
{
assert(a.length, "Attempting to fetch the front of an empty array of " ~ T.stringof);
return a[0];
}
///
pure nothrow @nogc @safe unittest
{
enum a = [1, 2, 3].staticArray;
static assert(a[].front == 1);
}
/// Implements the range interface primitive `empty` for types that obey $(LREF hasLength) property
@property bool empty(T)(auto ref scope T a)
if (is(typeof(a.length) : size_t))
{
return !a.length;
}
///
pure nothrow @nogc @safe unittest
{
enum a = [1, 2, 3].staticArray;
static assert(!a.empty);
static assert(a[3 .. $].empty);
}
pure nothrow @safe unittest
{
int[string] b;
assert(b.empty);
b["zero"] = 0;
assert(!b.empty);
}
/// Implements the range interface primitive `popFront` for built-in arrays.
void popFront(T)(/*scope*/ ref inout(T)[] array) pure nothrow @nogc @safe
{ // does not compile with GDC 9 if this is `scope`
assert(array.length, "Attempting to popFront() past the end of an array of " ~ T.stringof);
array = array[1 .. $];
}
///
pure nothrow @nogc @safe unittest
{
auto a = [1, 2, 3].staticArray;
auto b = a[];
auto expected = [2, 3].staticArray;
b.popFront();
assert(b == expected[]);
}