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