1 /** Reference Counted Array.
2 	See_Also: http://dpaste.dzfl.pl/817283c163f5
3  */
4 module nxt.rcstring;
5 
6 import core.memory : GC;
7 // import core.stdc.stdlib;
8 // import core.stdc.string;
9 // import std.algorithm;
10 
11 /** Reference Counted (RC) version of string.
12  */
13 alias RCString = RCXString!(immutable char);
14 
15 /** Reference Counted Array.
16 	Configured with character type `E`, maximum length for the small string optimization,
17 	and the allocation function, which must have the same semantics as `realloc`.
18 
19 	See_Also: https://github.com/burner/std.rcstring
20 */
21 struct RCXString(E = immutable char, size_t maxSmallSize = 23, alias realloc = GC.realloc)
22 {
23 	pure nothrow:
24 
25 	// Preconditions
26 	static assert(is(E == immutable), "Only immutable characters supported for now.");
27 	static assert(E.alignof <= 4, "Character type must be 32-bit aligned at most.");
28 	static assert(E.min == 0, "Character type must be unsigned.");
29 	static assert((maxSmallSize + 1) * E.sizeof % size_t.sizeof == 0,
30 				  "maxSmallSize + 1 must be a multiple of size_t.sizeof.");
31 	static assert((maxSmallSize + 1) * E.sizeof >= 3 * size_t.sizeof,
32 				  "maxSmallSize + 1 must be >= size_t.sizeof * 3.");
33 	static assert(maxSmallSize < E.max, "maxSmallSize must be less than E.max");
34 
35 	enum maxSmallLength = maxSmallSize;
36 
37 private:
38 	// import std.utf;
39 	import core.lifetime : emplace;
40 	import std.traits: isSomeChar, Unqual;
41 
42 	version (unittest) import std.stdio;
43 
44 	alias ME = Unqual!E; // mutable E
45 
46 	enum isString = isSomeChar!E;
47 
48 	// Simple reference-counted buffer. The reference count itself is a E. Layout is a size_t (the capacity)
49 	// followed by the reference count followed by the payload.
50 	struct RCBuffer
51 	{
52 		size_t capacity;
53 		uint refCount;
54 
55 		// Data starts right after the refcount, no padding because of the static assert above
56 		ME* mptr() @nogc { return cast(ME*) (&refCount + 1); }
57 		E* ptr() @nogc { return cast(E*) mptr; }
58 
59 		// Create a new buffer given capacity and initializes payload. Capacity must be large enough.
60 		static RCBuffer* make(in size_t capacity, const(ME)[] content)
61 		{
62 			assert(capacity >= content.length);
63 			auto result = cast(RCBuffer*) realloc(null, size_t.sizeof + uint.sizeof + capacity * E.sizeof);
64 			result || assert(0);
65 			result.capacity = capacity;
66 			result.refCount = 1;
67 			result.mptr[0 .. content.length] = content;
68 			return result;
69 		}
70 
71 		// Resize the buffer. It is assumed the reference count is 1.
72 		static void resize(ref RCBuffer* p, in size_t capacity)
73 		{
74 			assert(p.refCount == 1);
75 			p = cast(RCBuffer*) realloc(p, size_t.sizeof + uint.sizeof + capacity * E.sizeof);
76 			p || assert(0);
77 			p.capacity = capacity;
78 		}
79 
80 		unittest
81 		{
82 			auto p = make(101, null);
83 			assert(p.refCount == 1);
84 			assert(p.capacity == 101);
85 			resize(p, 203);
86 			assert(p.refCount == 1);
87 			assert(p.capacity == 203);
88 			realloc(p, 0);
89 		}
90 	}
91 
92 	// Hosts a large string
93 	struct Large
94 	{
95 		// <layout>
96 		union
97 		{
98 			immutable RCBuffer* buf;
99 			RCBuffer* mbuf;
100 		}
101 		union
102 		{
103 			E* ptr;
104 			ME* mptr;
105 		}
106 		static if ((maxSmallSize + 1) * E.sizeof == 3 * size_t.sizeof)
107 		{
108 			/* The small buffer and the large buffer overlap. This means the large buffer must give up its last byte
109 			 * as a discriminator.
110 			 */
111 			size_t _length;
112 			enum maxLarge = size_t.max >> (8 * E.sizeof);
113 			version (BigEndian)
114 			{
115 				// Use the LSB to store the marker
116 				size_t length() const @safe @nogc { return _length >> 8 * E.sizeof; }
117 				void length(size_t s) @safe @nogc { _length = Marker.isRefCounted | (s << (8 * E.sizeof)); }
118 			}
119 			else version (LittleEndian)
120 			{
121 				// Use the MSB to store the marker
122 				private enum size_t mask = size_t(E.max) << (8 * (size_t.sizeof - E.sizeof));
123 				size_t length() const @safe @nogc { return _length & ~mask; }
124 				void length(size_t s) @safe @nogc { assert(s <= maxLarge); _length = s | mask; }
125 			}
126 			else
127 			{
128 				static assert(0, "Unspecified endianness.");
129 			}
130 		}
131 		else
132 		{
133 			// No tricks needed, store the size plainly
134 			size_t _length;
135 			size_t length() const @safe @nogc
136 			{
137 				return _length;
138 			}
139 			void length(size_t s) @safe @nogc
140 			{
141 				_length = s;
142 			}
143 		}
144 		// </layout>
145 
146 		// Get length
147 		alias opDollar = length;
148 
149 		// Initializes a Large given capacity and content. Capacity must be at least as large as content's size.
150 		this(in size_t capacity, const(ME)[] content)
151 		{
152 			assert(capacity >= content.length);
153 			mbuf = RCBuffer.make(capacity, content);
154 			mptr = mbuf.mptr;
155 			length = content.length;
156 		}
157 
158 		// Initializes a Large from a string by copying it.
159 		this(const(ME)[] s)
160 		{
161 			this(s.length, s);
162 		}
163 
164 		static if (isString) unittest
165 		{
166 			const(ME)[] s1 = "hello, world";
167 			auto lrg1 = Large(s1);
168 			assert(lrg1.length == 12);
169 			immutable lrg2 = immutable Large(s1);
170 			assert(lrg2.length == 12);
171 			const lrg3 = const Large(s1);
172 			assert(lrg3.length == 12);
173 		}
174 
175 		// Initializes a Large from a static string by referring to it.
176 		this(immutable(ME)[] s)
177 		{
178 			assert(buf is null);
179 			ptr = s.ptr;
180 			length = s.length;
181 		}
182 
183 		static if (isString) unittest
184 		{
185 			immutable ME[] s = "abcdef";
186 			auto lrg1 = Large(s);
187 			assert(lrg1.length == 6);
188 			assert(lrg1.buf is null);
189 		}
190 
191 		// Decrements the reference count and frees buf if it goes down to zero.
192 		void decRef() nothrow
193 		{
194 			if (!mbuf) return;
195 			if (mbuf.refCount == 1) realloc(mbuf, 0);
196 			else --mbuf.refCount;
197 		}
198 
199 		auto opSlice() inout return
200 		{
201 			assert(ptr);
202 			return ptr[0 .. length];
203 		}
204 
205 		// Makes sure there's room for at least newCap Chars.
206 		void reserve(in size_t newCapacity)
207 		{
208 			if (mbuf && mbuf.refCount == 1 && mbuf.capacity >= newCapacity) return;
209 			immutable size = this.length;
210 			version (assert) scope(exit) assert(size == this.length);
211 			if (!mbuf)
212 			{
213 				// Migrate from static string to allocated string
214 				mbuf = RCBuffer.make(newCapacity, ptr[0 .. size]);
215 				ptr = mbuf.ptr;
216 				return;
217 			}
218 			if (mbuf.refCount > 1)
219 			{
220 				// Split this guy making its buffer unique
221 				--mbuf.refCount;
222 				mbuf = RCBuffer.make(newCapacity, ptr[0 .. size]);
223 				ptr = mbuf.ptr;
224 				// size stays untouched
225 			}
226 			else
227 			{
228 				immutable offset = ptr - mbuf.ptr;
229 				// If offset is too large, it's worth decRef()ing and then allocating a new buffer
230 				if (offset * 2 >= newCapacity)
231 				{
232 					auto newBuf = RCBuffer.make(newCapacity, ptr[0 .. size]);
233 					decRef;
234 					mbuf = newBuf;
235 					ptr = mbuf.ptr;
236 				}
237 				else
238 				{
239 					RCBuffer.resize(mbuf, newCapacity);
240 					ptr = mbuf.ptr + offset;
241 				}
242 			}
243 		}
244 
245 		unittest
246 		{
247 			Large obj;
248 			obj.reserve(1);
249 			assert(obj.mbuf !is null);
250 			assert(obj.mbuf.capacity >= 1);
251 			obj.reserve(1000);
252 			assert(obj.mbuf.capacity >= 1000);
253 			obj.reserve(10000);
254 			assert(obj.mbuf.capacity >= 10000);
255 		}
256 	}
257 
258 	// <layout>
259 	union
260 	{
261 		Large large;
262 		struct
263 		{
264 			union
265 			{
266 				E[maxSmallSize] small;
267 				ME[maxSmallSize] msmall;
268 			}
269 			ME smallLength;
270 		}
271 		size_t[(maxSmallSize + 1) / size_t.sizeof] ancillary; // used internally
272 	}
273 	// </layout>
274 
275 	hash_t toHash() const @trusted
276 	{
277 		import core.internal.hash : hashOf;
278 		return this.asSlice.hashOf;
279 	}
280 
281 	static if (isString) unittest
282 	{
283 		assert(RCXString("a").toHash ==
284 			   RCXString("a").toHash);
285 		assert(RCXString("a").toHash !=
286 			   RCXString("b").toHash);
287 		assert(RCXString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").toHash ==
288 			   RCXString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").toHash);
289 		assert(RCXString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").toHash !=
290 			   RCXString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").toHash);
291 	}
292 
293 	static if (isString) unittest
294 	{
295 		RCXString x;
296 		assert(x.smallLength == 0);
297 		assert(x.length == 0);
298 		x.large.length = 133;
299 		assert(x.smallLength == E.max);
300 		assert(x.large.length == 133);
301 		x.large.length = 0x0088_8888_8888_8888;
302 		assert(x.large.length == 0x0088_8888_8888_8888);
303 		assert(x.smallLength == E.max);
304 	}
305 
306 	// is this string small?
307 	bool isSmall() const @safe @nogc
308 	{
309 		return smallLength <= maxSmallSize;
310 	}
311 
312 	// release all memory associated with this
313 	private void decRef() @nogc
314 	{
315 		if (!isSmall) large.decRef;
316 	}
317 
318 	// Return a slice with the string's contents
319 	// Not public because it leaks the internals
320 	auto asSlice() inout @nogc
321 	{
322 		immutable s = smallLength;
323 		if (s <= maxSmallSize) return small.ptr[0 .. s];
324 		return large[];
325 	}
326 
327 public:
328 
329 	/// Returns the length of the string
330 	size_t length() const @nogc
331 	{
332 		immutable s = smallLength;
333 		return s <= maxSmallSize ? s : large.length;
334 	}
335 	/// Ditto
336 	alias opDollar = length;
337 
338 	static if (isString) unittest
339 	{
340 		auto s1 = RCXString("123456789_");
341 		assert(s1.length == 10);
342 		s1 ~= RCXString("123456789_123456789_123456789_123456789_12345");
343 		assert(s1.length == 55);
344 	}
345 
346 	/// Needed for correct printing in other modules
347 	static if (isString)
348 	{
349 		string toArray() const @trusted
350 		{
351 			return this.asSlice;
352 		}
353 	}
354 
355 	/** Construct a `RCXString` from a slice `s`.
356 
357 		If the slice is immutable, assumes the slice is a literal or
358 		GC-allocated and does NOT copy it internally.
359 
360 		Warning: Subsequently deallocating `s` will cause the `RCXString`
361 		to dangle. If the slice has `const` or mutable characters, creates
362 		and manages a copy internally.
363 	 */
364 	this(C)(C[] s)
365 		if (is(Unqual!C == ME))
366 	{
367 		// Contents is immutable, we may assume it won't go away ever
368 		if (s.length <= maxSmallSize)
369 		{
370 			// fits in small
371 			small[0 .. s.length] = s[]; // so copy it
372 			smallLength = cast(E)s.length;
373 		}
374 		else
375 		{
376 			emplace(&large, s);
377 		}
378 	}
379 
380 	// Test construction from immutable(ME)[], const(ME)[], and ME[]
381 	static if (isString) unittest
382 	{
383 		immutable(E)[] a = "123456789_";
384 		auto s1 = RCXString(a);
385 		assert(s1 == a);
386 		assert(s1.asSlice !is a, "Small strings must be copied");
387 		a = "123456789_123456789_123456789_123456789_";
388 		auto s2 = RCXString(a);
389 		assert(s2 == a);
390 		assert(s2.asSlice is a, "Large immutable strings shall not be copied");
391 
392 		const(char)[] b = "123456789_";
393 		auto s3 = RCXString(b);
394 		assert(s3 == b);
395 		assert(s3.isSmall, "Small strings must be copied");
396 		b = "123456789_123456789_123456789_123456789_";
397 		auto s4 = RCXString(b);
398 		assert(s4 == b);
399 		assert(s4.asSlice !is b, "Large non-immutable strings shall be copied");
400 
401 		char[] c = "123456789_".dup;
402 		auto s5 = RCXString(c);
403 		assert(s5 == c);
404 		assert(s5.isSmall, "Small strings must be copied");
405 		c = "123456789_123456789_123456789_123456789_".dup;
406 		auto s6 = RCXString(c);
407 		assert(s6 == c);
408 		assert(s6.asSlice !is c, "Large non-immutable strings shall be copied");
409 	}
410 
411 	static if (isString) unittest
412 	{
413 		const(ME)[] s = "123456789_123456789_123456789_123456789_";
414 		auto s1 = RCXString(s);
415 		assert(s1.large.mbuf);
416 		auto s2 = s1;
417 		assert(s1.large.mbuf is s2.large.mbuf);
418 		assert(s1.large.mbuf.refCount == 2);
419 		s1 = s ~ "123";
420 		assert(s1.large.mbuf.refCount == 1);
421 		assert(s2.large.mbuf.refCount == 1);
422 		assert(s2 == s);
423 		assert(s1 == s ~ "123");
424 		const s3 = s1;
425 		assert(s1.large.mbuf.refCount == 2);
426 		immutable s4 = s1;
427 		//immutable s5 = s3;
428 		assert(s1.large.mbuf.refCount == 3);
429 	}
430 
431 	// Postblit
432 	this(this) @nogc
433 	{
434 		if (!isSmall && large.mbuf) ++large.mbuf.refCount;
435 	}
436 
437 	// Dtor decrements refcount and may deallocate
438 	~this() nothrow @nogc
439 	{
440 		decRef;
441 	}
442 
443 	// Assigns another string
444 	void opAssign(immutable(ME)[] s)
445 	{
446 		decRef;
447 		// Contents is immutable, we may assume it won't go away ever
448 		emplace(&this, s);
449 	}
450 
451 	static if (isString) unittest
452 	{
453 		immutable(ME)[] s = "123456789_";
454 		RCXString rcs;
455 		rcs = s;
456 		assert(rcs.isSmall);
457 		s = "123456789_123456789_123456789_123456789_";
458 		rcs = s;
459 		assert(!rcs.isSmall);
460 		assert(rcs.large.mbuf is null);
461 	}
462 
463 	// Assigns another string
464 	void opAssign(const(ME)[] s)
465 	{
466 		if (capacity >= s.length)
467 		{
468 			// Noice, there's room
469 			if (s.length <= maxSmallSize)
470 			{
471 				// Fits in small
472 				msmall[0 .. s.length] = s[];
473 				smallLength = cast(E) s.length;
474 			}
475 			else
476 			{
477 				// Large it is
478 				assert(!isSmall);
479 				large.mptr[0 .. s.length] = s;
480 				large.length = s.length;
481 			}
482 		}
483 		else
484 		{
485 			// Tear down and rebuild
486 			decRef;
487 			emplace(&this, s);
488 		}
489 	}
490 
491 	static if (isString) unittest
492 	{
493 		const(ME)[] s = "123456789_123456789_123456789_123456789_";
494 		RCXString s1;
495 		s1 = s;
496 		assert(!s1.isSmall && s1.large.buf !is null);
497 		auto p = s1.ptr;
498 		s1 = s;
499 		assert(s1.ptr is p, "Wasteful reallocation");
500 		RCXString s2;
501 		s2 = s1;
502 		assert(s1.large.mbuf is s2.large.mbuf);
503 		assert(s1.large.mbuf.refCount == 2);
504 		s1 = "123456789_123456789_123456789_123456789_123456789_";
505 		assert(s1.large.mbuf !is s2.large.mbuf);
506 		assert(s1.large.mbuf is null);
507 		assert(s2.large.mbuf.refCount == 1);
508 		assert(s1 == "123456789_123456789_123456789_123456789_123456789_");
509 		assert(s2 == "123456789_123456789_123456789_123456789_");
510 	}
511 
512 	bool opEquals(const(ME)[] s) const @nogc
513 	{
514 		if (isSmall) return s.length == smallLength && small[0 .. s.length] == s;
515 		return large[] == s;
516 	}
517 
518 	bool opEquals(in RCXString s) const => this == s.asSlice;
519 
520 	static if (isString) unittest
521 	{
522 		const s1 = RCXString("123456789_123456789_123456789_123456789_123456789_");
523 		RCXString s2 = s1[0 .. 10];
524 		auto s3 = RCXString("123456789_");
525 		assert(s2 == s3);
526 	}
527 
528 	/** Returns the maximum number of character this string can store without
529 		requesting more memory.
530 	 */
531 	size_t capacity() const @property @nogc
532 	{
533 		/** This is subtle: if large.mbuf is null (i.e. the string had been constructed from a literal), then the
534 			capacity is maxSmallSize because that's what we can store without a memory (re)allocation. Same if refCount is
535 			greater than 1 - we can't reuse the memory.
536 		*/
537 		return isSmall || !large.mbuf || large.mbuf.refCount > 1 ? maxSmallSize : large.mbuf.capacity;
538 	}
539 
540 	static if (isString) unittest
541 	{
542 		auto s = RCXString("abc");
543 		assert(s.capacity == maxSmallSize);
544 		s = "123456789_123456789_123456789_123456789_123456789_";
545 		assert(s.capacity == maxSmallSize);
546 		const char[] lit = "123456789_123456789_123456789_123456789_123456789_";
547 		s = lit;
548 		assert(s.capacity >= 50);
549 	}
550 
551 	void reserve(in size_t capacity)
552 	{
553 		if (isSmall)
554 		{
555 			if (capacity <= maxSmallSize)
556 			{
557 				// stays small
558 				return;
559 			}
560 			// small to large
561 			immutable length = smallLength;
562 			auto newLayout = Large(capacity, small.ptr[0 .. length]);
563 			large = newLayout;
564 		}
565 		else
566 		{
567 			// large to large
568 			if (large.mbuf && large.mbuf.capacity >= capacity) return;
569 			large.reserve(capacity);
570 		}
571 	}
572 
573 	static if (isString) unittest
574 	{
575 		RCXString s1;
576 		s1.reserve(1);
577 		assert(s1.capacity >= 1);
578 		s1.reserve(1023);
579 		assert(s1.capacity >= 1023);
580 		s1.reserve(10230);
581 		assert(s1.capacity >= 10230);
582 	}
583 
584 	/** Appends `s` to `this`.
585 	 */
586 	void opOpAssign(string s : "~")(const(ME)[] s)
587 	{
588 		immutable length = this.length;
589 		immutable newLen = length + s.length;
590 		if (isSmall)
591 		{
592 			if (newLen <= maxSmallSize)
593 			{
594 				// stays small
595 				msmall[length .. newLen] = s;
596 				smallLength = cast(E) newLen;
597 			}
598 			else
599 			{
600 				// small to large
601 				auto newLayout = Large(newLen, small.ptr[0 .. length]);
602 				newLayout.mptr[length .. newLen][] = s;
603 				newLayout.length = newLen;
604 				large = newLayout;
605 				assert(!isSmall);
606 				assert(this.length == newLen);
607 			}
608 		}
609 		else
610 		{
611 			// large to large
612 			large.reserve(newLen);
613 			large.mptr[length .. newLen][] = s;
614 			large.length = newLen;
615 		}
616 	}
617 
618 	static if (isString) unittest
619 	{
620 		auto s1 = RCXString("123456789_123456789_123456789_123456789_");
621 		s1 ~= s1;
622 		assert(s1 == "123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_");
623 		foreach (i; 0 .. 70) s1.popFront();
624 		assert(s1 == "123456789_");
625 		s1 ~= "abc";
626 		assert(s1 == "123456789_abc");
627 	}
628 
629 	/// Ditto
630 	void opOpAssign(string s : "~")(const auto ref RCXString s)
631 	{
632 		this ~= s.asSlice;
633 	}
634 
635 	static if (isString) unittest
636 	{
637 		RCXString s1;
638 		s1 = "hello";
639 		assert(s1 == "hello");
640 		s1 ~= ", world! ";
641 		assert(s1 == "hello, world! ");
642 		s1 ~= s1;
643 		assert(s1 == "hello, world! hello, world! ");
644 		s1 ~= s1;
645 		assert(s1 == "hello, world! hello, world! hello, world! hello, world! ");
646 		auto s2 = RCXString("yah! ");
647 		assert(s2 == "yah! ");
648 		s2 ~= s1;
649 		assert(s2 == "yah! hello, world! hello, world! hello, world! hello, world! ");
650 		s2 = "123456789_123456789_123456789_123456789_";
651 		s2 ~= s2;
652 		assert(s2 == "123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_");
653 		auto s3 = s2;
654 		assert(s3.large.mbuf.refCount == 2);
655 		s2 ~= "123456789_";
656 		assert(s2.large.mbuf.refCount == 1);
657 		assert(s3.large.mbuf.refCount == 1);
658 		assert(s3 == "123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_");
659 
660 		s2 = "123456789_123456789_123456789_123456789_";
661 		const s4 = RCXString(", world");
662 		s2 ~= s4;
663 		assert(s2 == "123456789_123456789_123456789_123456789_, world");
664 		s2 ~= const RCXString("!!!");
665 		assert(s2 == "123456789_123456789_123456789_123456789_, world!!!");
666 	}
667 
668 	/// Returns `true` iff `this` is empty
669 	bool empty() const @property @nogc => !length;
670 
671 	static if (isString)
672 	{
673 		private dchar unsafeDecode(const(ME)* p) const @nogc
674 		{
675 			byte c = *p;
676 			dchar res = c & 0b0111_1111;
677 			if (c >= 0) return res;
678 			assert(c < 0b1111_1000);
679 			dchar cover = 0b1000_0000;
680 			c <<= 1;
681 			assert(c < 0);
682 			do
683 			{
684 				++p;
685 				assert((*p >> 6) == 0b10);
686 				cover <<= 5;
687 				res = (res << 6) ^ *p ^ cover ^ 0b1000_0000;
688 				c <<= 1;
689 			} while(c < 0);
690 			return res;
691 		}
692 	}
693 
694 	/// Returns the first code point of `this`.
695 	auto front() const @property @nogc in(!empty)
696 	{
697 		/+ TODO: make safe +/
698 		static if (isString)
699 			return unsafeDecode(ptr);
700 		else
701 			return ptr[0];
702 	}
703 
704 	/// Returns the last code point of `this`.
705 	static if (isString)
706 	{
707 		dchar back() const @property @nogc in(!empty)
708 		{
709 			auto p = ptr + length - 1;
710 			if (*p < 0b1000_0000)
711 				return *p;
712 			/+ TODO: make safe +/
713 			do
714 			{
715 				--p;
716 			} while (!(*p & 0b0100_0000));
717 			return unsafeDecode(p);
718 		}
719 	}
720 	else
721 		E back() const @property @nogc => ptr[length - 1];
722 
723 	/// Returns the `n`th code unit in `this`.
724 	E opIndex(size_t n) const @nogc in(n < length) => ptr[n];
725 
726 	static if (isString) unittest
727 	{
728 		auto s1 = RCXString("hello");
729 		assert(s1.front == 'h');
730 		assert(s1[1] == 'e');
731 		assert(s1.back == 'o');
732 		assert(s1[$ - 1] == 'o');
733 		s1 = RCXString("Ü");
734 		assert(s1.length == 2);
735 		assert(s1.front == 'Ü');
736 		assert(s1.back == 'Ü');
737 	}
738 
739 	/// Discards the first code point
740 	void popFront() @nogc
741 	{
742 		assert(!empty && ptr);
743 		uint toPop = 1;
744 		auto b = *ptr;
745 		if (b >= 0b1000_0000)
746 		{
747 			toPop = (b | 0b0010_0000) != b ? 2
748 				: (b | 0b0001_0000) != b ? 3
749 				: 4;
750 		}
751 		if (isSmall)
752 		{
753 			// Must shuffle in place
754 			/+ TODO: make faster +/
755 			foreach (i; 0 .. length - toPop)
756 				msmall[i] = small[i + toPop];
757 			smallLength -= toPop;
758 		}
759 		else
760 		{
761 			large.ptr += toPop;
762 			large.length = large.length - toPop;
763 		}
764 	}
765 
766 	static if (isString) unittest
767 	{
768 		auto s1 = RCXString("123456789_");
769 		auto s2 = s1;
770 		s1.popFront();
771 		assert(s1 == "23456789_");
772 		assert(s2 == "123456789_");
773 		s1 = RCXString("123456789_123456789_123456789_123456789_");
774 		s2 = s1;
775 		s1.popFront();
776 		assert(s1 == "23456789_123456789_123456789_123456789_");
777 		assert(s2 == "123456789_123456789_123456789_123456789_");
778 		s1 = "öü";
779 		s2 = s1;
780 		s1.popFront();
781 		assert(s1 == "ü");
782 		assert(s2 == "öü");
783 	}
784 
785 	/// Discards the last code point
786 	void popBack() @nogc
787 	{
788 		assert(!empty && ptr);
789 		auto p = ptr + length - 1;
790 		if (*p < 0b1000_0000)
791 		{
792 			// hot path
793 			if (isSmall) --smallLength;
794 			else large.length = large.length - 1;
795 			return;
796 		}
797 		/+ TODO: make safe +/
798 		auto p1 = p;
799 		do
800 		{
801 			--p;
802 		} while (!(*p & 0b0100_0000));
803 		immutable diff = p1 - p + 1;
804 		assert(diff > 1 && diff <= length);
805 		if (isSmall) smallLength -= diff;
806 		else large.length = large.length - diff;
807 	}
808 
809 	static if (isString) unittest
810 	{
811 		auto s1 = RCXString("123456789_");
812 		auto s2 = s1;
813 		s1.popBack;
814 		assert(s1 == "123456789");
815 		assert(s2 == "123456789_");
816 		s1 = RCXString("123456789_123456789_123456789_123456789_");
817 		s2 = s1;
818 		s1.popBack;
819 		assert(s1 == "123456789_123456789_123456789_123456789");
820 		assert(s2 == "123456789_123456789_123456789_123456789_");
821 		s1 = "öü";
822 		s2 = s1;
823 		s1.popBack;
824 		assert(s1 == "ö");
825 		assert(s2 == "öü");
826 	}
827 
828 	/// Returns a slice to the entire string or a portion of it.
829 	auto opSlice() inout @nogc
830 	{
831 		return this;
832 	}
833 
834 	/// Ditto
835 	auto opSlice(size_t b, size_t e) inout
836 	{
837 		assert(b <= e && e <= length);
838 		auto ptr = this.ptr;
839 		auto sz = e - b;
840 		if (sz <= maxSmallSize)
841 		{
842 			// result is small
843 			RCXString result = void;
844 			result.msmall[0 .. sz] = ptr[b .. e];
845 			result.smallLength = cast(E) sz;
846 			return result;
847 		}
848 		assert(!isSmall);
849 		RCXString result = this;
850 		result.large.ptr += b;
851 		result.large.length = e - b;
852 		return result;
853 	}
854 
855 	static if (isString) unittest
856 	{
857 		immutable s = RCXString("123456789_123456789_123456789_123456789");
858 		RCXString s1 = s[0 .. 38];
859 		assert(!s1.isSmall && s1.large.buf is null);
860 	}
861 
862 	// Unsafe! Returns a pointer to the beginning of the payload.
863 	auto ptr() inout @nogc
864 	{
865 		return isSmall ? small.ptr : large.ptr;
866 	}
867 
868 	static if (isString) unittest
869 	{
870 		auto s1 = RCXString("hello");
871 		auto s2 = s1[1 .. $ - 1];
872 		assert(s2 == "ell");
873 		s1 = "123456789_123456789_123456789_123456789_";
874 		s2 = s1[1 .. $ - 1];
875 		assert(s2 == "23456789_123456789_123456789_123456789");
876 	}
877 
878 	/// Returns the concatenation of `this` with `s`.
879 	RCXString opBinary(string s = "~")(const auto ref RCXString s) const
880 	{
881 		return this ~ s.asSlice;
882 	}
883 
884 	/// Ditto
885 	RCXString opBinary(string s = "~")(const(ME)[] s) const
886 	{
887 		immutable length = this.length;
888 		auto resultLen = length + s.length;
889 		RCXString result = void;
890 		if (resultLen <= maxSmallSize)
891 		{
892 			// noice
893 			result.msmall.ptr[0 .. length] = ptr[0 .. length];
894 			result.msmall.ptr[length .. resultLen] = s[];
895 			result.smallLength = cast(E) resultLen;
896 			return result;
897 		}
898 		emplace(&result.large, resultLen, this.asSlice);
899 		result ~= s;
900 		return result;
901 	}
902 
903 	/// Returns the concatenation of `s` with `this`.
904 	RCXString opBinaryRight(string s = "~")(const(E)[] s) const
905 	{
906 		immutable length = this.length, resultLen = length + s.length;
907 		RCXString result = void;
908 		if (resultLen <= maxSmallSize)
909 		{
910 			// noice
911 			result.msmall.ptr[0 .. s.length] = s[];
912 			result.msmall.ptr[s.length .. resultLen] = small.ptr[0 .. length];
913 			result.smallLength = cast(E) resultLen;
914 			return result;
915 		}
916 		emplace(&result.large, resultLen, s);
917 		result ~= this;
918 		return result;
919 	}
920 
921 	static if (isString) unittest
922 	{
923 		auto s1 = RCXString("hello");
924 		auto s2 = s1 ~ ", world!";
925 		assert(s2 == "hello, world!");
926 		s1 = "123456789_123456789_123456789_123456789_";
927 		s2 = s1 ~ "abcdefghi_";
928 		assert(s2 == "123456789_123456789_123456789_123456789_abcdefghi_");
929 		s2 = s1 ~ s1;
930 		assert(s2 == "123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_");
931 		s2 = "abcdefghi_" ~ s1;
932 		assert(s2 == "abcdefghi_123456789_123456789_123456789_123456789_");
933 	}
934 }
935 
936 unittest {
937 	alias RCI = RCXString!(immutable uint);
938 	RCI x;
939 }
940 
941 /// verify UTF-8 storage
942 unittest {
943 	string s = "åäö";
944 	RCString rcs = s;
945 	assert(rcs.length == 6);
946 	import std.algorithm : count;
947 	assert(rcs.count == 3);
948 	assert(rcs.front == 'å');
949 	rcs.popFront();
950 	assert(rcs.front == 'ä');
951 	rcs.popFront();
952 	assert(rcs.front == 'ö');
953 	rcs.popFront();
954 	assert(rcs.empty);
955 }
956 
957 version = profile;
958 
959 /// shows performance increase for SSO over built-in string
960 version (profile) unittest {
961 	enum maxSmallSize = 23;
962 	alias S = RCXString!(immutable char, maxSmallSize);
963 
964 	import std.datetime: StopWatch, Duration;
965 	import std.conv : to;
966 	import std.stdio;
967 
968 	enum n = 2^^21;
969 
970 	StopWatch sw;
971 
972 	sw.reset;
973 	sw.start;
974 	char[maxSmallSize] ss;
975 	foreach (i; 0 .. n)
976 	{
977 		auto x = S(ss);
978 	}
979 	sw.stop;
980 	auto timeRCString = sw.peek().msecs;
981 	writeln("> RCString took ", sw.peek().to!Duration);
982 
983 	sw.reset;
984 	sw.start;
985 	foreach (i; 0 .. n)
986 	{
987 		string x = ss.idup;
988 	}
989 	sw.stop;
990 	auto timeString = sw.peek().msecs;
991 	writeln("> Builtin string took ", sw.peek().to!Duration);
992 
993 	writeln("> Speedup: ", timeString/timeRCString);
994 }