1 /** Statically allocated arrays with compile-time known lengths.
2  */
3 module nxt.fixed_array;
4 
5 @safe pure:
6 
7 /** Statically allocated `T`-array of fixed pre-allocated length.
8  *
9  * Similar to Rust's `fixedvec`: https://docs.rs/fixedvec/0.2.3/fixedvec/
10  * Similar to `mir.small_array` at http://mir-algorithm.libmir.org/mir_small_array.html.
11  *
12  * TODO Merge member functions with basic_*_array.d and array_ex.d
13  *
14  * TODO Add @safe nothrow @nogc ctor from static array (of known length)
15  *
16  * TODO use opPostMove (https://github.com/dlang/DIPs/blob/master/DIPs/accepted/DIP1014.md)
17  */
18 struct FixedArray(T, uint capacity_, bool borrowChecked = false)
19 {
20     // pragma(msg, "T:", T, " capacity_:", capacity_, " borrowChecked:", borrowChecked);
21     import core.exception : onRangeError;
22     import core.lifetime : move, moveEmplace;
23     import std.bitmanip : bitfields;
24     import std.traits : isSomeChar, isAssignable;
25     import core.internal.traits : hasElaborateDestructor;
26     import nxt.container_traits : isAddress;
27 
28     alias capacity = capacity_; // for public use
29 
30     /// Store of `capacity` number of elements.
31     T[capacity] _store;         // TODO use store constructor
32 
33     static if (borrowChecked)
34     {
35         /// Number of bits needed to store number of read borrows.
36         private enum readBorrowCountBits = 3;
37 
38         /// Maximum value possible for `_readBorrowCount`.
39         enum readBorrowCountMax = 2^^readBorrowCountBits - 1;
40 
41         static      if (capacity <= 2^^(8*ubyte.sizeof - 1 - readBorrowCountBits) - 1)
42         {
43             private enum lengthMax = 2^^4 - 1;
44             alias Length = ubyte;
45             // TODO make private:
46             mixin(bitfields!(Length, "_length", 4, /// number of defined elements in `_store`
47                              bool, "_writeBorrowed", 1,
48                              uint, "_readBorrowCount", readBorrowCountBits,
49                       ));
50         }
51         else static if (capacity <= 2^^(8*ushort.sizeof - 1 - readBorrowCountBits) - 1)
52         {
53             alias Length = ushort;
54             private enum lengthMax = 2^^14 - 1;
55             // TODO make private:
56             mixin(bitfields!(Length, "_length", 14, /// number of defined elements in `_store`
57                              bool, "_writeBorrowed", 1,
58                              uint, "_readBorrowCount", readBorrowCountBits,
59                       ));
60         }
61         else
62         {
63             static assert("Too large requested capacity " ~ capacity);
64         }
65     }
66     else
67     {
68         static if (capacity <= ubyte.max)
69         {
70             static if (T.sizeof == 1)
71                 alias Length = ubyte; // pack length
72             else
73                 alias Length = uint;
74         }
75         else static if (capacity <= ushort.max)
76         {
77             static if (T.sizeof <= 2)
78                 alias Length = uint; // pack length
79             else
80                 alias Length = uint;
81         }
82         else
83         {
84             static assert("Too large requested capacity " ~ capacity);
85         }
86         Length _length;         /// number of defined elements in `_store`
87     }
88 
89     /// Is `true` iff `U` can be assign to the element type `T` of `this`.
90     private enum isElementAssignable(U) = isAssignable!(T, U);
91 
92     /// Construct from element `values`.
93     this(Us...)(Us values) @trusted
94     if (Us.length <= capacity)
95     {
96         foreach (immutable ix, ref value; values)
97         {
98             import nxt.container_traits : needsMove;
99             static if (needsMove!(typeof(value)))
100             {
101                 moveEmplace(value, _store[ix]);
102             }
103             else
104             {
105                 _store[ix] = value;
106             }
107         }
108         _length = cast(Length)values.length;
109         static if (borrowChecked)
110         {
111             _writeBorrowed = false;
112             _readBorrowCount = 0;
113         }
114     }
115 
116     /// Construct from element `values`.
117     this(U)(U[] values) @trusted
118     if (__traits(isCopyable, U)//  &&
119         // TODO isElementAssignable!U
120         ) // prevent accidental move of l-value `values` in array calls
121     {
122         version(assert) if (values.length > capacity) onRangeError(); // `Arguments don't fit in array`
123         _store[0 .. values.length] = values;
124         _length = cast(Length)values.length;
125         static if (borrowChecked)
126         {
127             _writeBorrowed = false;
128             _readBorrowCount = 0;
129         }
130     }
131 
132     /// Construct from element `values`.
133     static typeof(this) fromValuesUnsafe(U)(U[] values) @system
134     if (__traits(isCopyable, U) &&
135         isElementAssignable!U
136         ) // prevent accidental move of l-value `values` in array calls
137     {
138         typeof(return) that;              // TODO use Store constructor:
139 
140         that._store[0 .. values.length] = values;
141         that._length = cast(Length)values.length;
142 
143         static if (borrowChecked)
144         {
145             that._writeBorrowed = false;
146             that._readBorrowCount = 0;
147         }
148 
149         return that;
150     }
151 
152     static if (borrowChecked ||
153                hasElaborateDestructor!T)
154     {
155         /** Destruct. */
156         ~this() @trusted @nogc
157         {
158             static if (borrowChecked) { assert(!isBorrowed); }
159             static if (hasElaborateDestructor!T)
160             {
161                 foreach (immutable i; 0 .. length)
162                 {
163                     .destroy(_store.ptr[i]);
164                 }
165             }
166         }
167     }
168 
169     /** Add elements `es` to the back.
170      * Throws when array becomes full.
171      * NOTE doesn't invalidate any borrow
172      */
173     void insertBack(Es...)(Es es) @trusted
174     if (Es.length <= capacity) // TODO use `isAssignable`
175     {
176         version(assert) if (_length + Es.length > capacity) onRangeError(); // `Arguments don't fit in array`
177         foreach (immutable i, ref e; es)
178         {
179             moveEmplace(e, _store[_length + i]); // TODO remove `move` when compiler does it for us
180         }
181         _length = cast(Length)(_length + Es.length); // TODO better?
182     }
183     /// ditto
184     alias put = insertBack;       // `OutputRange` support
185 
186     /** Try to add elements `es` to the back.
187      * NOTE doesn't invalidate any borrow
188      * Returns: `true` iff all `es` were pushed, `false` otherwise.
189      */
190     bool insertBackMaybe(Es...)(Es es) @trusted
191     if (Es.length <= capacity)
192     {
193         if (_length + Es.length > capacity) { return false; }
194         foreach (immutable i, ref e; es)
195         {
196             moveEmplace(e, _store[_length + i]); // TODO remove `move` when compiler does it for us
197         }
198         _length = cast(Length)(_length + Es.length); // TODO better?
199         return true;
200     }
201     /// ditto
202     alias putMaybe = insertBackMaybe;
203 
204     /** Add elements `es` to the back.
205      * NOTE doesn't invalidate any borrow
206      */
207     void opOpAssign(string op, Us...)(Us values)
208     if (op == "~" &&
209         values.length >= 1 &&
210         allSatisfy!(isElementAssignable, Us))
211     {
212         insertBack(values.move()); // TODO remove `move` when compiler does it for
213     }
214 
215     import std.traits : isMutable;
216     static if (isMutable!T)
217     {
218         /** Pop first (front) element. */
219         auto ref popFront()
220         {
221             assert(!empty);
222             static if (borrowChecked) { assert(!isBorrowed); }
223             // TODO is there a reusable Phobos function for this?
224             foreach (immutable i; 0 .. _length - 1)
225             {
226                 move(_store[i + 1], _store[i]); // like `_store[i] = _store[i + 1];` but more generic
227             }
228             _length = cast(typeof(_length))(_length - 1); // TODO better?
229             return this;
230         }
231     }
232 
233     /** Pop last (back) element. */
234     void popBack()()            // template-lazy
235     {
236         assert(!empty);
237         static if (borrowChecked) { assert(!isBorrowed); }
238         _length = cast(Length)(_length - 1); // TODO better?
239         static if (hasElaborateDestructor!T)
240         {
241             .destroy(_store.ptr[_length]);
242         }
243         else static if (isAddress!T)
244         {
245             _store.ptr[_length] = null; // please the GC
246         }
247     }
248 
249     /** Pop the `n` last (back) elements. */
250     void popBackN()(size_t n)   // template-lazy
251     {
252         assert(length >= n);
253         static if (borrowChecked) { assert(!isBorrowed); }
254         _length = cast(Length)(_length - n); // TODO better?
255         static if (hasElaborateDestructor!T)
256         {
257             foreach (i; 0 .. n)
258             {
259                 .destroy(_store.ptr[_length + i]);
260             }
261         }
262         else static if (isAddress!T) // please the GC
263         {
264             foreach (const i; 0 .. n)
265             {
266                 _store.ptr[_length + i] = null;
267             }
268         }
269     }
270 
271     /** Move element at `index` to return. */
272     static if (isMutable!T)
273     {
274         /** Pop element at `index`. */
275         void popAt()(size_t index) // template-lazy
276         @trusted
277         @("complexity", "O(length)")
278         {
279             assert(index < this.length);
280             .destroy(_store.ptr[index]);
281             shiftToFrontAt(index);
282             _length = cast(Length)(_length - 1);
283         }
284 
285         T moveAt()(size_t index) // template-lazy
286         @trusted
287         @("complexity", "O(length)")
288         {
289             assert(index < this.length);
290             auto value = _store.ptr[index].move();
291             shiftToFrontAt(index);
292             _length = cast(Length)(_length - 1);
293             return value;
294         }
295 
296         private void shiftToFrontAt()(size_t index) // template-lazy
297             @trusted
298         {
299             foreach (immutable i; 0 .. this.length - (index + 1))
300             {
301                 immutable si = index + i + 1; // source index
302                 immutable ti = index + i;     // target index
303                 moveEmplace(_store.ptr[si],
304                             _store.ptr[ti]);
305             }
306         }
307     }
308 
309 pragma(inline, true):
310 
311     /** Index operator. */
312     ref inout(T) opIndex(size_t i) inout @trusted return
313     {
314         assert(i < _length);
315         return _store.ptr[i];
316     }
317 
318     /** First (front) element. */
319     ref inout(T) front() inout @trusted return
320     {
321         assert(!empty);
322         return _store.ptr[0];
323     }
324 
325     /** Last (back) element. */
326     ref inout(T) back() inout @trusted return
327     {
328         assert(!empty);
329         return _store.ptr[_length - 1];
330     }
331 
332     static if (borrowChecked)
333     {
334         import nxt.borrowed : ReadBorrowed, WriteBorrowed;
335 
336         /// Get read-only slice in range `i` .. `j`.
337         auto opSlice(size_t i, size_t j) const return scope { return sliceRO(i, j); }
338         /// Get read-write slice in range `i` .. `j`.
339         auto opSlice(size_t i, size_t j) return scope { return sliceRW(i, j); }
340 
341         /// Get read-only full slice.
342         auto opSlice() const return scope { return sliceRO(); }
343         /// Get read-write full slice.
344         auto opSlice() return scope { return sliceRW(); }
345 
346         /// Get full read-only slice.
347         ReadBorrowed!(T[], typeof(this)) sliceRO() const @trusted return scope
348         {
349             import core.internal.traits : Unqual;
350             assert(!_writeBorrowed, "Already write-borrowed");
351             return typeof(return)(_store.ptr[0 .. _length],
352                                   cast(Unqual!(typeof(this))*)(&this)); // trusted unconst casta
353         }
354 
355         /// Get read-only slice in range `i` .. `j`.
356         ReadBorrowed!(T[], typeof(this)) sliceRO(size_t i, size_t j) const @trusted return scope
357         {
358             import core.internal.traits : Unqual;
359             assert(!_writeBorrowed, "Already write-borrowed");
360             return typeof(return)(_store.ptr[i .. j],
361                                   cast(Unqual!(typeof(this))*)(&this)); // trusted unconst cast
362         }
363 
364         /// Get full read-write slice.
365         WriteBorrowed!(T[], typeof(this)) sliceRW() @trusted return scope
366         {
367             assert(!_writeBorrowed, "Already write-borrowed");
368             assert(_readBorrowCount == 0, "Already read-borrowed");
369             return typeof(return)(_store.ptr[0 .. _length], &this);
370         }
371 
372         /// Get read-write slice in range `i` .. `j`.
373         WriteBorrowed!(T[], typeof(this)) sliceRW(size_t i, size_t j) @trusted return scope
374         {
375             assert(!_writeBorrowed, "Already write-borrowed");
376             assert(_readBorrowCount == 0, "Already read-borrowed");
377             return typeof(return)(_store.ptr[0 .. j], &this);
378         }
379 
380         @property
381         {
382             /// Returns: `true` iff `this` is either write or read borrowed.
383             bool isBorrowed() const { return _writeBorrowed || _readBorrowCount >= 1; }
384 
385             /// Returns: `true` iff `this` is write borrowed.
386             bool isWriteBorrowed() const { return _writeBorrowed; }
387 
388             /// Returns: number of read-only borrowers of `this`.
389             uint readBorrowCount() const { return _readBorrowCount; }
390         }
391     }
392     else
393     {
394         /// Get slice in range `i` .. `j`.
395         inout(T)[] opSlice(size_t i, size_t j) @trusted inout return scope
396         {
397             // assert(i <= j);
398             // assert(j <= _length);
399             return _store[i .. j]; // TODO make .ptr work
400         }
401 
402         /// Get full slice.
403         inout(T)[] opSlice() @trusted inout return scope
404         {
405             return _store[0 .. _length]; // TODO make .ptr work
406         }
407     }
408 
409     @property
410     {
411         /** Returns: `true` iff `this` is empty, `false` otherwise. */
412         bool empty() const { return _length == 0; }
413 
414         /** Returns: `true` iff `this` is full, `false` otherwise. */
415         bool full() const { return _length == capacity; }
416 
417         /** Get length. */
418         auto length() const { return _length; }
419         alias opDollar = length;    /// ditto
420 
421         static if (isSomeChar!T)
422         {
423             /** Get as `string`. */
424             scope const(T)[] toString() const return
425             {
426                 return opSlice();
427             }
428         }
429     }
430 
431     /** Comparison for equality. */
432     bool opEquals()(const scope auto ref typeof(this) rhs) const
433     {
434         return this[] == rhs[];
435     }
436     /// ditto
437     bool opEquals(U)(const scope U[] rhs) const
438     if (is(typeof(T[].init == U[].init)))
439     {
440         return this[] == rhs;
441     }
442 }
443 
444 /** Stack-allocated string of maximum length of `capacity.`
445  *
446  * Similar to `mir.small_string` at http://mir-algorithm.libmir.org/mir_small_string.html.
447  */
448 alias StringN(uint capacity, bool borrowChecked = false) = FixedArray!(immutable(char), capacity, borrowChecked);
449 
450 /** Stack-allocated wstring of maximum length of `capacity.` */
451 alias WStringN(uint capacity, bool borrowChecked = false) = FixedArray!(immutable(wchar), capacity, borrowChecked);
452 
453 /** Stack-allocated dstring of maximum length of `capacity.` */
454 alias DStringN(uint capacity, bool borrowChecked = false) = FixedArray!(immutable(dchar), capacity, borrowChecked);
455 
456 /** Stack-allocated mutable string of maximum length of `capacity.` */
457 alias MutableStringN(uint capacity, bool borrowChecked = false) = FixedArray!(char, capacity, borrowChecked);
458 
459 /** Stack-allocated mutable wstring of maximum length of `capacity.` */
460 alias MutableWStringN(uint capacity, bool borrowChecked = false) = FixedArray!(char, capacity, borrowChecked);
461 
462 /** Stack-allocated mutable dstring of maximum length of `capacity.` */
463 alias MutableDStringN(uint capacity, bool borrowChecked = false) = FixedArray!(char, capacity, borrowChecked);
464 
465 /// construct from array may throw
466 @safe pure unittest
467 {
468     enum capacity = 3;
469     alias T = int;
470     alias A = FixedArray!(T, capacity);
471     static assert(!mustAddGCRange!A);
472 
473     auto a = A([1, 2, 3].s[]);
474     assert(a[] == [1, 2, 3].s);
475 }
476 
477 /// unsafe construct from array
478 @trusted pure nothrow @nogc unittest
479 {
480     enum capacity = 3;
481     alias T = int;
482     alias A = FixedArray!(T, capacity);
483     static assert(!mustAddGCRange!A);
484 
485     auto a = A.fromValuesUnsafe([1, 2, 3].s);
486     assert(a[] == [1, 2, 3].s);
487 }
488 
489 /// construct from scalars is nothrow
490 @safe pure nothrow @nogc unittest
491 {
492     enum capacity = 3;
493     alias T = int;
494     alias A = FixedArray!(T, capacity);
495     static assert(!mustAddGCRange!A);
496 
497     auto a = A(1, 2, 3);
498     assert(a[] == [1, 2, 3].s);
499 
500     static assert(!__traits(compiles, { auto _ = A(1, 2, 3, 4); }));
501 }
502 
503 /// scope checked string
504 @safe pure unittest
505 {
506     enum capacity = 15;
507     foreach (StrN; AliasSeq!(StringN// , WStringN, DStringN
508                  ))
509     {
510         alias String15 = StrN!(capacity);
511 
512         typeof(String15.init[0])[] xs;
513         auto x = String15("alphas");
514 
515         assert(x[0] == 'a');
516         assert(x[$ - 1] == 's');
517 
518         assert(x[0 .. 2] == "al");
519         assert(x[] == "alphas");
520 
521         const y = String15("åäö_åäöå"); // fits in 15 chars
522     }
523 }
524 
525 /// scope checked string
526 pure unittest
527 {
528     enum capacity = 15;
529     foreach (Str; AliasSeq!(StringN!capacity,
530                             WStringN!capacity,
531                             DStringN!capacity))
532     {
533         static assert(!mustAddGCRange!Str);
534         static if (isDIP1000)
535         {
536             static assert(!__traits(compiles, {
537                         auto f() @safe pure
538                         {
539                             auto x = Str("alphas");
540                             auto y = x[];
541                             return y;   // errors with -dip1000
542                         }
543                     }));
544         }
545     }
546 }
547 
548 @safe pure unittest
549 {
550     static assert(mustAddGCRange!(FixedArray!(string, 1, false)));
551     static assert(mustAddGCRange!(FixedArray!(string, 1, true)));
552     static assert(mustAddGCRange!(FixedArray!(string, 2, false)));
553     static assert(mustAddGCRange!(FixedArray!(string, 2, true)));
554 }
555 
556 ///
557 @safe pure unittest
558 {
559     import std.exception : assertNotThrown;
560 
561     alias T = char;
562     enum capacity = 3;
563 
564     alias A = FixedArray!(T, capacity, true);
565     static assert(!mustAddGCRange!A);
566     static assert(A.sizeof == T.sizeof*capacity + 1);
567 
568     import std.range.primitives : isOutputRange;
569     static assert(isOutputRange!(A, T));
570 
571     auto ab = A("ab");
572     assert(!ab.empty);
573     assert(ab[0] == 'a');
574     assert(ab.front == 'a');
575     assert(ab.back == 'b');
576     assert(ab.length == 2);
577     assert(ab[] == "ab");
578     assert(ab[0 .. 1] == "a");
579     assertNotThrown(ab.insertBack('_'));
580     assert(ab[] == "ab_");
581     ab.popBack();
582     assert(ab[] == "ab");
583     assert(ab.toString == "ab");
584 
585     ab.popBackN(2);
586     assert(ab.empty);
587     assertNotThrown(ab.insertBack('a', 'b'));
588 
589     const abc = A("abc");
590     assert(!abc.empty);
591     assert(abc.front == 'a');
592     assert(abc.back == 'c');
593     assert(abc.length == 3);
594     assert(abc[] == "abc");
595     assert(ab[0 .. 2] == "ab");
596     assert(abc.full);
597     static assert(!__traits(compiles, { const abcd = A('a', 'b', 'c', 'd'); })); // too many elements
598 
599     assert(ab[] == "ab");
600     ab.popFront();
601     assert(ab[] == "b");
602 
603     const xy = A("xy");
604     assert(!xy.empty);
605     assert(xy[0] == 'x');
606     assert(xy.front == 'x');
607     assert(xy.back == 'y');
608     assert(xy.length == 2);
609     assert(xy[] == "xy");
610     assert(xy[0 .. 1] == "x");
611 
612     const xyz = A("xyz");
613     assert(!xyz.empty);
614     assert(xyz.front == 'x');
615     assert(xyz.back == 'z');
616     assert(xyz.length == 3);
617     assert(xyz[] == "xyz");
618     assert(xyz.full);
619     static assert(!__traits(compiles, { const xyzw = A('x', 'y', 'z', 'w'); })); // too many elements
620 }
621 
622 ///
623 @safe pure unittest
624 {
625     static void testAsSomeString(T)()
626     {
627         enum capacity = 15;
628         alias A = FixedArray!(immutable(T), capacity);
629         static assert(!mustAddGCRange!A);
630         auto a = A("abc");
631         assert(a[] == "abc");
632         assert(a[].equal("abc"));
633 
634         import std.conv : to;
635         const x = "a".to!(T[]);
636     }
637 
638     foreach (T; AliasSeq!(char// , wchar, dchar
639                  ))
640     {
641         testAsSomeString!T();
642     }
643 }
644 
645 /// equality
646 @safe pure unittest
647 {
648     enum capacity = 15;
649     alias S = FixedArray!(int, capacity);
650     static assert(!mustAddGCRange!S);
651 
652     assert(S([1, 2, 3].s[]) ==
653            S([1, 2, 3].s[]));
654     assert(S([1, 2, 3].s[]) ==
655            [1, 2, 3]);
656 }
657 
658 @safe pure unittest
659 {
660     class C { int value; }
661     alias S = FixedArray!(C, 2);
662     static assert(mustAddGCRange!S);
663 }
664 
665 /// `insertBackMaybe` is nothrow @nogc.
666 @safe pure nothrow @nogc unittest
667 {
668     alias S = FixedArray!(int, 2);
669     S s;
670     assert(s.insertBackMaybe(42));
671     assert(s.insertBackMaybe(43));
672     assert(!s.insertBackMaybe(0));
673     assert(s.length == 2);
674 }
675 
676 /// equality
677 @system pure nothrow @nogc unittest
678 {
679     enum capacity = 15;
680     alias S = FixedArray!(int, capacity);
681 
682     assert(S.fromValuesUnsafe([1, 2, 3].s) ==
683            S.fromValuesUnsafe([1, 2, 3].s));
684 
685     const ax = [1, 2, 3].s;
686     assert(S.fromValuesUnsafe([1, 2, 3].s) == ax);
687     assert(S.fromValuesUnsafe([1, 2, 3].s) == ax[]);
688 
689     const cx = [1, 2, 3].s;
690     assert(S.fromValuesUnsafe([1, 2, 3].s) == cx);
691     assert(S.fromValuesUnsafe([1, 2, 3].s) == cx[]);
692 
693     immutable ix = [1, 2, 3].s;
694     assert(S.fromValuesUnsafe([1, 2, 3].s) == ix);
695     assert(S.fromValuesUnsafe([1, 2, 3].s) == ix[]);
696 }
697 
698 /// assignment from `const` to `immutable` element type
699 @safe pure unittest
700 {
701     enum capacity = 15;
702     alias String15 = StringN!(capacity);
703     static assert(!mustAddGCRange!String15);
704 
705     const char[4] _ = ['a', 'b', 'c', 'd'];
706     auto x = String15(_[]);
707     assert(x.length == 4);
708     assert(x[] == "abcd");
709 }
710 
711 /// borrow checking
712 @system pure unittest
713 {
714     enum capacity = 15;
715     alias String15 = StringN!(capacity, true);
716     static assert(String15.readBorrowCountMax == 7);
717     static assert(!mustAddGCRange!String15);
718 
719     auto x = String15("alpha");
720 
721     assert(x[].equal("alpha") &&
722            x[].equal("alpha"));
723 
724     {
725         auto xw1 = x[];
726         assert(x.isWriteBorrowed);
727         assert(x.isBorrowed);
728     }
729 
730     auto xr1 = (cast(const)x)[];
731     assert(x.readBorrowCount == 1);
732 
733     auto xr2 = (cast(const)x)[];
734     assert(x.readBorrowCount == 2);
735 
736     auto xr3 = (cast(const)x)[];
737     assert(x.readBorrowCount == 3);
738 
739     auto xr4 = (cast(const)x)[];
740     assert(x.readBorrowCount == 4);
741 
742     auto xr5 = (cast(const)x)[];
743     assert(x.readBorrowCount == 5);
744 
745     auto xr6 = (cast(const)x)[];
746     assert(x.readBorrowCount == 6);
747 
748     auto xr7 = (cast(const)x)[];
749     assert(x.readBorrowCount == 7);
750 
751     assertThrown!AssertError((cast(const)x)[]);
752 }
753 
754 version(unittest)
755 {
756     import std.algorithm.comparison : equal;
757     import std.meta : AliasSeq;
758     import std.exception : assertThrown;
759     import core.exception : AssertError;
760 
761     import nxt.array_help : s;
762     import nxt.container_traits : mustAddGCRange;
763     import nxt.dip_traits : isDIP1000;
764 }