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