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