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