1 module nxt.sso_string;
2 
3 /** Small-size-optimized (SSO) variant of `string`.
4  *
5  * Storage is placed on the stack if the number of `char`s is less than
6  * `smallCapacity`, otherwise as a normal (large) `string`. The large `string`
7  * will be allocated on the GC-heap if the `SSOString` is constructed from a
8  * non-`string` (non-`immutable` `char[]`) parameter.
9  *
10  * Because `SSOString` doesn't have a destructor it can safely allocate using a
11  * GC-backed region allocator without relying on a GC finalizer.
12  *
13  * In order to mimic `string/array/slice`-behaviour, opCast returns `false` for
14  * `SSOString()` and `true` for `SSOString("")`. This requires `SSOString()` to
15  * default to a large string in which large pointer is set to `null`.
16  *
17  * NOTE big-endian platform support hasn't been verified.
18  *
19  * TODO Add to Phobos' std.typecons or std.array or std.string
20  *
21  * See_Also: https://forum.dlang.org/post/pb87rn$2icb$1@digitalmars.com
22  * See_Also: https://issues.dlang.org/show_bug.cgi?id=18792
23  *
24  * TODO Use extra bits in `Short.length` for these special text encodings:
25  * - 5-bit lowercase English letter into 128/5 = 25 chars
26  * - 5-bit uppercase English letter into 120/5 = 25 chars
27  * - 6-bit mixedcase English letter into 120/6 = 20 chars
28  */
29 struct SSOString
30 {
31 @safe:
32 
33     private alias E = char;     // element type
34 
35     @property void toString(scope void delegate(const(E)[]) @safe sink) const
36     {
37         sink(opSlice());
38     }
39 
40 pure:
41 
42     /** Return `this` lowercased. */
43     typeof(this) toLower()() const @trusted // template-lazy
44     {
45         if (isSmallASCII)
46         {
47             typeof(return) result = void;
48             result.small.length = small.length;
49             foreach (const index; 0 .. smallCapacity)
50             {
51                 import std.ascii : toLower;
52                 (cast(E[])(result.small.data))[index] = toLower(small.data[index]);
53             }
54             return result;
55         }
56         else
57         {
58             if (isLarge)
59             {
60                 import std.uni : asLowerCase;
61                 import std.conv : to;
62                 return typeof(return)(opSlice().asLowerCase.to!string); // TODO make .to!string nothrow
63             }
64             else // small non-ASCII can usually be performed without GC-allocation
65             {
66                 typeof(return) result = this; // copy
67                 import std.uni : toLowerInPlace;
68                 auto slice = cast(E[])(result.opSlice()); // need ref to slice
69                 toLowerInPlace(slice);
70                 if (slice is result.opSlice() || // no reallocation
71                     slice.length == result.length) // or same length (happens for German double-s)
72                 {
73                     return result;
74                 }
75                 else
76                 {
77                     version(none)
78                     {
79                         import nxt.dbgio;
80                         dbg(`toLowerInPlace reallocated from "`,
81                             result.opSlice(), `" of length `, result.opSlice().length,
82                             ` to "`
83                             , slice, `" of length `, slice.length);
84                     }
85                     return typeof(return)(slice); // reallocation occurred
86                 }
87             }
88         }
89     }
90 
91     /** Return `this` uppercased. */
92     typeof(this) toUpper()() const @trusted // template-lazy
93     {
94         if (isSmallASCII)
95         {
96             typeof(return) result = void;
97             result.small.length = small.length;
98             foreach (const index; 0 .. smallCapacity)
99             {
100                 import std.ascii : toUpper;
101                 (cast(E[])(result.small.data))[index] = toUpper(small.data[index]);
102             }
103             return result;
104         }
105         else
106         {
107             if (isLarge)
108             {
109                 import std.uni : asUpperCase;
110                 import std.conv : to;
111                 return typeof(return)(opSlice().asUpperCase.to!string); // TODO make .to!string nothrow
112             }
113             else // small non-ASCII can usually be performed without GC-allocation
114             {
115                 typeof(return) result = this; // copy
116                 import std.uni : toUpperInPlace;
117                 auto slice = cast(E[])(result.opSlice()); // need ref to slice
118                 toUpperInPlace(slice);
119                 if (slice is result.opSlice() || // no reallocation
120                     slice.length == result.length) // or same length (happens for German double-s)
121                 {
122                     return result;
123                 }
124                 else
125                 {
126                     version(none)
127                     {
128                         import nxt.dbgio;
129                         dbg(`toUpperInPlace reallocated from "`,
130                             result.opSlice(), `" of length `, result.opSlice().length,
131                             ` to "`
132                             , slice, `" of length `, slice.length);
133                     }
134                     return typeof(return)(slice); // reallocation occurred
135                 }
136             }
137         }
138     }
139 
140     pure nothrow:
141 
142     /** Construct from `source`, which potentially needs GC-allocation (iff
143      * `source.length > smallCapacity` and `source` is not a `string`).
144      */
145     this(Chars)(const scope auto ref Chars source) @trusted
146     if (isCharArray!(typeof(source[]))) // not immutable `E`
147     {
148         static if (__traits(isStaticArray, Chars))
149         {
150             static if (source.length <= smallCapacity) // inferred @nogc
151             {
152                 // pragma(msg, "Small static array source of length ", Chars.length);
153                 small.data[0 .. source.length] = source;
154                 small.length = cast(typeof(small.length))(encodeSmallLength(source.length));
155             }
156             else
157             {
158                 // pragma(msg, "Large static array source of length ", Chars.length);
159                 static if (is(typeof(source[0]) == immutable(E)))
160                 {
161                     large = source; // already immutable so no duplication needed
162                 }
163                 else
164                 {
165                     large = source.idup; // GC-allocate
166                 }
167                 raw.length = encodeLargeLength(raw.length);
168             }
169         }
170         else                    // `Chars` is a (dynamic) array slice
171         {
172             if (source.length <= smallCapacity)
173             {
174                 (cast(char*)small.data.ptr)[0 .. source.length] = source;
175                 small.length = cast(typeof(small.length))(encodeSmallLength(source.length));
176             }
177             else
178             {
179                 static if (is(typeof(source[0]) == immutable(E)))
180                 {
181                     large = source; // already immutable so no duplication needed
182                 }
183                 else
184                 {
185                     large = source.idup; // GC-allocate
186                 }
187                 raw.length = encodeLargeLength(raw.length);
188             }
189         }
190     }
191 
192     import std.traits : isIterable;
193 
194     this(Source)(Source source)
195     if (isIterable!(Source) &&
196         is(ElementType!Source : dchar))
197     {
198         import std.utf : encode;
199 
200         static assert(0, "TODO complete this function");
201 
202         // pre-calculate number of `char`s needed
203         size_t precount = 0;
204         foreach (const dch; source)
205         {
206             char[4] chars;
207             precount += encode(chars, dch);
208         }
209 
210         if (precount <= smallCapacity)
211         {
212             size_t offset = 0;
213             foreach (const dch; source)
214             {
215                 char[4] chars;
216                 offset += encode(chars, dch);
217             }
218             small.length = offset;
219         }
220         else
221         {
222             assert(0);
223         }
224     }
225 
226     /** Return `this` converted to a `string`, without any GC-allocation because
227      * `this` is `immutable`.
228      */
229     @property string toString() immutable @trusted pure nothrow @nogc // never allocates
230     {
231         return opSlice();
232     }
233 
234     /** Return `this` converted to a `string`, which potentially needs
235      * GC-allocation (iff `length > smallCapacity`).
236      *
237      * implementation kept in sync with `opSlice`.
238      */
239     @property string toString() const return @trusted pure nothrow // may GC-allocate
240     {
241         if (isLarge)
242         {
243             // GC-allocated slice has immutable members so ok to cast
244             return cast(typeof(return))raw.ptr[0 .. decodeRawLength(raw.length)]; // no allocation
245         }
246         else
247         {
248             return small.data.ptr[0 .. decodeRawLength(small.length)].idup; // need duplicate to make `immutable`
249         }
250     }
251 
252     @nogc:
253 
254     /** Get hash of `this`, with extra fast computation for the small case.
255      */
256     @property hash_t toHash() const scope @trusted
257     {
258         version(LDC) pragma(inline, true);
259         if (isLarge)
260         {
261             import core.internal.hash : hashOf;
262             return hashOf(opSlice());
263         }
264         else                    // fast path for small string
265         {
266             import nxt.hash_functions : wangMixHash64;
267             return (wangMixHash64(words[0] >> 1) ^ // shift away LS-bit being a constant for a small string
268                     wangMixHash64(words[1]));
269         }
270     }
271 
272     /** Get length. */
273     @property size_t length() const scope @trusted
274     {
275         pragma(inline, true);
276         if (isLarge)
277         {
278             return decodeRawLength(large.length); // skip first bit
279         }
280         else
281         {
282             return decodeRawLength(small.length); // skip fist bit
283         }
284     }
285     /// ditto
286     alias opDollar = length;
287 
288     /** Check if `this` is empty. */
289     @property bool empty() const scope @safe pure nothrow @nogc { return length() == 0; }
290 
291     /** Check if `this` is `null`. */
292     @property bool isNull() const scope @safe pure nothrow @nogc { return this == typeof(this).init; }
293 
294     /** Return a slice to either the whole large or whole small `string`.
295      *
296      * Implementation is kept in sync with `toString`.
297      */
298     inout(E)[] opSlice() inout return scope @trusted @nogc
299     {
300         if (isLarge)
301         {
302             return cast(typeof(return))raw.ptr[0 .. decodeRawLength(raw.length)]; // no allocation
303             // return getLarge();
304         }
305         else
306         {
307             return cast(typeof(return))small.data.ptr[0 .. decodeRawLength(small.length)]; // scoped
308             // return getSmall();
309         }
310     }
311 
312     /** Return a slice at `[i .. j]` to either the internally stored large or small `string`.
313      *
314      * Implementation is kept in sync with `toString`.
315      */
316     inout(E)[] opSlice(size_t i, size_t j) inout return @safe
317     {
318         pragma(inline, true);
319         return opSlice()[i .. j];
320     }
321 
322     private inout(E)[] getLarge() inout return scope @trusted @nogc
323     {
324         return cast(typeof(return))raw.ptr[0 .. decodeRawLength(raw.length)]; // no allocation
325         // alternative:  return large.ptr[0 .. large.length/2];
326     }
327 
328     private inout(E)[] getSmall() inout return scope @trusted @nogc
329     {
330         return cast(typeof(return))small.data.ptr[0 .. decodeRawLength(small.length)]; // scoped
331     }
332 
333     /** Return the `index`ed `char` of `this`.
334      */
335     ref inout(E) opIndex(size_t index) inout return @trusted
336     {
337         pragma(inline, true);
338         return opSlice()[index]; // does range check
339     }
340 
341     /// Get pointer to the internally stored `char`s.
342     @property private immutable(E)* ptr() const return @trusted
343     {
344         if (isLarge)
345         {
346             return large.ptr;   // GC-heap pointer
347         }
348         else
349         {
350             return small.data.ptr; // stack pointer
351         }
352     }
353 
354     /** Check if `this` is equal to `rhs`. */
355     bool opEquals()(const scope auto ref typeof(this) rhs) const scope @trusted
356     {
357         pragma(inline, true);
358         return opSlice() == rhs.opSlice();
359     }
360 
361     /** Check if `this` is equal to `rhs`. */
362     bool opEquals()(const scope const(E)[] rhs) const scope @trusted
363     {
364         pragma(inline, true);
365         return opSlice() == rhs;
366     }
367 
368     /** Compare `this` with `that`.
369      *
370      * See_Also: https://forum.dlang.org/post/muhfypwftdivluqdbmdf@forum.dlang.org
371      */
372     @property int opCmp()(const scope typeof(this) that) const scope // template-lazy
373     {
374         pragma(inline, true);
375         auto a = this[];
376         auto b = that[];
377         return a < b ? -1 : (a > b);
378         // import core.internal.array.comparison : __cmp; // instead of `std.algorithm.comparison : cmp`;
379         // return __cmp(this[], that[]);
380     }
381 
382     bool opCast(T : bool)() const scope @trusted
383     {
384         pragma(inline, true);
385         if (isLarge)
386         {
387             return large !is null;
388         }
389         else
390         {
391             return small.length != 0;
392         }
393     }
394 
395     /** Check if is the same as to `rhs`.
396      *
397      * See_Also: https://forum.dlang.org/post/agzznbzkacfhyqvoezht@forum.dlang.org.
398      */
399     version(none)               // `is` operator cannot be overloaded. See: https://forum.dlang.org/post/prmrli$1146$1@digitalmars.com
400     bool opBinary(string op)(const scope auto ref typeof(this) rhs) const scope @trusted
401     if (op == `is`)         // TODO has not effect
402     {
403         pragma(inline, true);
404         return opSlice() == rhs.opSlice();
405     }
406 
407     /** Support trait `isNullable`. */
408     static immutable nullValue = typeof(this).init;
409 
410     /** Support trait `isHoleable`. */
411     static immutable holeValue = typeof(this).asHole();
412 
413     /** Check if this a hole, meaning a removed/erase value. */
414     bool isHole() const scope @safe nothrow @nogc
415     {
416         return words[0] == size_t.max;
417     }
418 
419     /** That this a hole, meaning a removed/erase value. */
420     void holeify() @system @nogc scope
421     {
422         words[0] = size_t.max;
423         words[1] = size_t.max;
424     }
425 
426     /** Returns: a holed `SSOString`, meaning a removed/erase value. */
427     private static typeof(this) asHole() @system
428     {
429         typeof(return) result = void;
430         result.holeify();
431         return result;
432     }
433 
434     /** Check if `this` is a small ASCII string. */
435     bool isSmallASCII() const scope @trusted
436     {
437         pragma(inline, true);
438         static assert(largeLengthTagBitOffset == 0);// bit 0 of lsbyte not set => small
439         // should be fast on 64-bit platforms:
440         return ((words[0] & 0x_80_80_80_80__80_80_80_01UL) == 1 && // bit 0 of lsbyte is set => small
441                 (words[1] & 0x_80_80_80_80__80_80_80_80UL) == 0);
442     }
443 
444 private:
445 
446     /** Returns: `true` iff this is a large string, otherwise `false.` */
447     @property bool isLarge() const scope @trusted
448     {
449         pragma(inline, true);
450         return !(large.length & (1 << largeLengthTagBitOffset)); // first bit discriminates small from large
451     }
452 
453     alias Large = immutable(E)[];
454 
455     public enum smallCapacity = Large.sizeof - Small.length.sizeof;
456     static assert(smallCapacity > 0, "No room for small source for immutable(E) being " ~ immutable(E).stringof);
457 
458     enum largeLengthTagBitOffset = 0; ///< bit position for large tag in length.
459     enum smallLengthBitCount = 4;
460     static assert(smallCapacity == 2^^smallLengthBitCount-1);
461 
462     enum metaBits = 3;               ///< Number of bits used for metadata.
463     enum metaMask = (2^^metaBits-1); ///< Mask for metadata shifted to bottom.
464     enum tagsBitCount = 1 + metaBits; ///< Number of bits used for small discriminator plus extra meta data.
465     static assert(smallLengthBitCount + tagsBitCount == 8);
466 
467     /// Get metadata byte with first `metaBits` bits set.
468     @property ubyte metadata() const @safe pure nothrow @nogc
469     {
470         return (small.length >> (1 << largeLengthTagBitOffset)) & metaMask; // git bits [1 .. 1+metaBits]
471     }
472 
473     /// Set metadata.
474     @property void metadata(ubyte data) @trusted pure nothrow @nogc
475     {
476         assert(data < (1 << metaBits));
477         if (isLarge)
478         {
479             raw.length = encodeLargeLength(length) | ((data & metaMask) << (largeLengthTagBitOffset + 1));
480         }
481         else
482         {
483             small.length = cast(ubyte)encodeSmallLength(length) | ((data & metaMask) << (largeLengthTagBitOffset + 1));
484         }
485     }
486 
487     /// Decode raw length `rawLength` by shifting away tag bits.
488     static size_t decodeRawLength(size_t rawLength) @safe pure nothrow @nogc
489     {
490         return rawLength >> tagsBitCount;
491     }
492 
493     /// Encode `Large` length from `Length`.
494     static size_t encodeLargeLength(size_t length) @safe pure nothrow @nogc
495     {
496         return (length << tagsBitCount);
497     }
498 
499     /// Encode `Small` length from `Length`.
500     static size_t encodeSmallLength(size_t length) @safe pure nothrow @nogc
501     {
502         assert(length <= smallCapacity);
503         return (length << tagsBitCount) | (1 << largeLengthTagBitOffset);
504     }
505 
506     version(LittleEndian) // see: http://forum.dlang.org/posting/zifyahfohbwavwkwbgmw
507     {
508         struct Small
509         {
510             /* TODO only first 4 bits are needed to represent a length between
511              * 0-15, use other 4 bits.
512              */
513             ubyte length = 0;
514             immutable(E)[smallCapacity] data = [0,0,0,0,0,
515                                                 0,0,0,0,0,
516                                                 0,0,0,0,0]; // explicit init needed for `__traits(isZeroInit)` to be true.
517         }
518     }
519     else
520     {
521         struct Small
522         {
523             immutable(E)[smallCapacity] data = [0,0,0,0,0,
524                                                 0,0,0,0,0,
525                                                 0,0,0,0,0]; // explicit init needed for `__traits(isZeroInit)` to be true.
526             /* TODO only first 4 bits are needed to represent a length between
527              * 0-15, use other 4 bits.
528              */
529             ubyte length;
530         }
531         static assert(0, "TODO add BigEndian support and test");
532     }
533 
534     struct Raw                  // same memory layout as `immutable(E)[]`
535     {
536         size_t length = 0;      // can be bit-fiddled without GC allocation
537         immutable(E)* ptr = null;
538     }
539 
540     union
541     {
542         Raw raw;
543         Large large;
544         Small small;
545         size_t[2] words;
546     }
547 }
548 version(unittest) static assert(SSOString.sizeof == string.sizeof);
549 
550 /// construct from non-immutable source is allowed in non-`@nogc` context
551 @safe pure nothrow unittest
552 {
553     alias S = SSOString;
554 
555     const char[] x0;
556     const s0 = S(x0);           // no .idup
557 
558     const char[] x16 = new char[16];
559     const s16 = S(x16);         // will call .idup
560 }
561 
562 /// construct from non-immutable source is not allowed in `@nogc` context
563 @safe pure nothrow @nogc unittest
564 {
565     alias S = SSOString;
566     const char[] s;
567     static assert(__traits(compiles, { const s0_ = S(s); }));
568 }
569 
570 /// test behaviour of `==` and `is` operator
571 @trusted pure nothrow @nogc unittest
572 {
573     alias S = SSOString;
574 
575     const S x = "42";
576     assert(!x.isNull);
577     assert(x == "42");
578 
579     const S y = "42";
580     assert(!y.isNull);
581     assert(y == "42");
582 
583     assert(x == y);
584     assert(x == y[]);
585     assert(x[] == y);
586     assert(x[] == y[]);
587     assert(x[] is x[]);
588     assert(y[] is y[]);
589     assert(x[] !is y[]);
590     assert(x.ptr !is y.ptr);
591 
592     const S z = "43";
593     assert(!z.isNull);
594     assert(z == "43");
595     assert(x != z);
596     assert(x[] != z[]);
597     assert(x !is z);
598     assert(x[] !is z[]);
599 }
600 
601 ///
602 @safe pure nothrow @nogc unittest
603 {
604     alias S = SSOString;
605 
606     static assert(S.smallCapacity == 15);
607 
608     import nxt.gc_traits : mustAddGCRange;
609     static assert(mustAddGCRange!S); // `Large large.ptr` must be scanned
610 
611     static assert(__traits(isZeroInit, S));
612     // TODO assert(S.init == S.nullValue);
613 
614     auto s0 = S.init;
615     assert(s0.isNull);
616     assert(s0.length == 0);
617     assert(s0.isLarge);
618     assert(s0[] == []);
619 
620     char[S.smallCapacity] charsSmallCapacity = "123456789_12345"; // fits in small string
621     const sSmallCapacity = S(charsSmallCapacity);
622     assert(!sSmallCapacity.isLarge);
623     assert(sSmallCapacity.length == S.smallCapacity);
624     assert(sSmallCapacity == charsSmallCapacity);
625 
626     const s0_ = S("");
627     assert(s0_.isNull);         // cannot distinguish
628     assert(s0 == s0_);
629 
630     const s7 = S("0123456");
631     assert(!s7.isNull);
632 
633     const s7_ = S("0123456_"[0 .. $ - 1]);
634     assert(s7.ptr !is s7_.ptr); // string data shall not overlap
635     assert(s7 == s7_);
636 
637     const _s7 = S("_0123456"[1 .. $]); // source from other string literal
638     assert(s7.ptr !is _s7.ptr); // string data shall not overlap
639     assert(s7 == _s7);
640 
641     assert(!s7.isLarge);
642     assert(s7.length == 7);
643     assert(s7[] == "0123456");
644     assert(s7[] == "_0123456"[1 .. $]);
645     assert(s7[] == "0123456_"[0 .. $ - 1]);
646     assert(s7[0 .. 4] == "0123");
647 
648     const s15 = S("0123456789abcde");
649     assert(!s15.isNull);
650     static assert(is(typeof(s15[]) == const(char)[]));
651     assert(!s15.isLarge);
652     assert(s15.length == 15);
653     assert(s15[] == "0123456789abcde");
654     assert(s15[0 .. 4] == "0123");
655     assert(s15[10 .. 15] == "abcde");
656     assert(s15[10 .. $] == "abcde");
657 
658     const s16 = S("0123456789abcdef");
659     assert(!s16.isNull);
660     static assert(is(typeof(s16[]) == const(char)[]));
661     assert(s16.isLarge);
662 
663     const s16_ = S("0123456789abcdef_"[0 .. s16.length]);
664     assert(s16.length == s16_.length);
665     assert(s16[] == s16_[]);
666     assert(s16.ptr !is s16_.ptr); // string data shall not overlap
667     assert(s16 == s16_);              // but contents is equal
668 
669     const _s16 = S("_0123456789abcdef"[1 .. $]);
670     assert(s16.length == _s16.length);
671     assert(s16[] == _s16[]);    // contents is equal
672     assert(s16 == _s16);        // contents is equal
673 
674     assert(s16.length == 16);
675     assert(s16[] == "0123456789abcdef");
676     assert(s16[0] == '0');
677     assert(s16[10] == 'a');
678     assert(s16[15] == 'f');
679     assert(s16[0 .. 4] == "0123");
680     assert(s16[10 .. 16] == "abcdef");
681     assert(s16[10 .. $] == "abcdef");
682 }
683 
684 /// metadata for null string
685 @safe pure nothrow @nogc unittest
686 {
687     alias S = SSOString;
688     auto s = S.init;
689     assert(s.isNull);
690     foreach (const i; 0 .. 8)
691     {
692         s.metadata = i;
693         assert(s.metadata == i);
694         assert(s.length == 0);
695         // TODO assert(!s.isNull);
696     }
697 }
698 
699 /// metadata for small string
700 @safe pure nothrow @nogc unittest
701 {
702     alias S = SSOString;
703     auto s = S("0123456");
704     assert(!s.isNull);
705     assert(!s.isLarge);
706     foreach (const i; 0 .. 8)
707     {
708         s.metadata = i;
709         assert(s.metadata == i);
710         assert(s.length == 7);
711         assert(!s.isLarge);
712         assert(!s.isNull);
713     }
714 }
715 
716 /// metadata for small string with maximum length
717 @safe pure nothrow @nogc unittest
718 {
719     alias S = SSOString;
720     auto s = S("0123456789abcde");
721     assert(s.length == S.smallCapacity);
722     assert(!s.isNull);
723     assert(!s.isLarge);
724     foreach (const i; 0 .. 8)
725     {
726         s.metadata = i;
727         assert(s.metadata == i);
728         assert(s.length == 15);
729         assert(!s.isLarge);
730         assert(!s.isNull);
731     }
732 }
733 
734 /// metadata for large string with minimum length
735 @safe pure nothrow @nogc unittest
736 {
737     alias S = SSOString;
738     auto s = S("0123456789abcdef");
739     assert(s.length == S.smallCapacity + 1);
740     assert(!s.isNull);
741     assert(s.isLarge);
742     foreach (const i; 0 .. 8)
743     {
744         s.metadata = i;
745         assert(s.metadata == i);
746         assert(s.length == 16);
747         assert(s.isLarge);
748         assert(!s.isNull);
749     }
750 }
751 
752 /// construct from static array larger than `smallCapacity`
753 @safe pure nothrow unittest
754 {
755     alias S = SSOString;
756     char[S.smallCapacity + 1] charsMinLargeCapacity;
757     const _ = S(charsMinLargeCapacity);
758 }
759 
760 /// hole handling
761 @trusted pure nothrow @nogc unittest
762 {
763     alias S = SSOString;
764     assert(!S.init.isHole);
765     assert(!S("").isHole);
766     assert(!S("a").isHole);
767     assert(S.asHole.isHole);
768 }
769 
770 /// DIP-1000 return ref escape analysis
771 @safe pure nothrow unittest
772 {
773     static if (isDIP1000)
774     {
775         alias S = SSOString;
776         static assert(!__traits(compiles, { immutable(char)* f1() @safe pure nothrow { S x; return x.ptr; } }));
777         static assert(!__traits(compiles, { string f1() @safe pure nothrow { S x; return x[]; } }));
778         static assert(!__traits(compiles, { string f2() @safe pure nothrow { S x; return x.toString; } }));
779         static assert(!__traits(compiles, { ref immutable(char) g() @safe pure nothrow @nogc { S x; return x[0]; } }));
780     }
781 }
782 
783 /// ASCII purity and case-conversion
784 @safe pure nothrow @nogc unittest
785 {
786     alias S = SSOString;
787 
788     // these are all small ASCII
789     assert( S("a").isSmallASCII);
790     assert( S("b").isSmallASCII);
791     assert( S("z").isSmallASCII);
792     assert( S("_").isSmallASCII);
793     assert( S("abcd").isSmallASCII);
794     assert( S("123456789_12345").isSmallASCII);
795 
796     // these are not
797     assert(!S("123456789_123456").isSmallASCII); // too large
798     assert(!S("123456789_123ö").isSmallASCII);
799     assert(!S("ö").isSmallASCII);
800     assert(!S("Ö").isSmallASCII);
801     assert(!S("åäö").isSmallASCII);
802     assert(!S("ö-värld").isSmallASCII);
803 }
804 
805 /// ASCII purity and case-conversion
806 @safe pure unittest
807 {
808     alias S = SSOString;
809     assert(S("A").toLower[] == "a");
810     assert(S("a").toUpper[] == "A");
811     assert(S("ABCDEFGHIJKLMNO").toLower[] == "abcdefghijklmno"); // small
812     assert(S("abcdefghijklmno").toUpper[] == "ABCDEFGHIJKLMNO"); // small
813     assert(S("ÅÄÖ").toLower[] == "åäö");
814     assert(S("åäö").toUpper[] == "ÅÄÖ");
815     assert(S("ABCDEFGHIJKLMNOP").toLower[] == "abcdefghijklmnop"); // large
816     assert(S("abcdefghijklmnop").toUpper[] == "ABCDEFGHIJKLMNOP"); // large
817 
818     char[6] x = "ÅÄÖ";
819     import std.uni : toLowerInPlace;
820     auto xref = x[];
821     toLowerInPlace(xref);
822     assert(x == "åäö");
823     assert(xref == "åäö");
824 }
825 
826 /// lexicographic comparison
827 @safe pure unittest
828 {
829     alias S = SSOString;
830 
831     const S a = S("a");
832     assert(a == S("a"));
833 
834     immutable S b = S("b");
835 
836     assert(a < b);
837     assert(b > a);
838     assert(a[] < b[]);
839 
840     assert("a" < "b");
841     assert("a" < "å");
842     assert("Å" < "å");
843     assert(S("a") < S("å"));
844     assert(S("ÅÄÖ") < S("åäö"));
845 }
846 
847 /// cast to bool
848 @safe pure unittest
849 {
850     alias S = SSOString;
851     // mimics behaviour of casting of `string` to `bool`
852     assert(!S());
853     assert(S(""));
854     assert(S("abc"));
855 }
856 
857 /// to string conversion
858 @safe pure unittest
859 {
860     alias S = SSOString;
861 
862     // mutable small will GC-allocate
863     {
864         S s = S("123456789_12345");
865         assert(s.ptr is &s.opSlice()[0]);
866         assert(s.ptr !is &s.toString()[0]);
867     }
868 
869     // const small will GC-allocate
870     {
871         const S s = S("123456789_12345");
872         assert(s.ptr is &s.opSlice()[0]);
873         assert(s.ptr !is &s.toString()[0]);
874     }
875 
876     // immutable small will not allocate
877     {
878         immutable S s = S("123456789_12345");
879         assert(s.ptr is &s.opSlice()[0]);
880         assert(s.ptr is &s.toString()[0]);
881         // TODO check return via -dip1000
882     }
883 
884     /* Forbid return of possibly locally scoped `Smll` small stack object
885      * regardless of head-mutability.
886      */
887     static if (isDIP1000)
888     {
889         static assert(!__traits(compiles, { immutable(char)* f1() @safe pure nothrow { S x; return x.ptr; } }));
890         static assert(!__traits(compiles, { immutable(char)* f1() @safe pure nothrow { const S x; return x.ptr; } }));
891         static assert(!__traits(compiles, { immutable(char)* f1() @safe pure nothrow { immutable S x; return x.ptr; } }));
892 
893         /** TODO Enable the following line when DIP-1000 works for opSlice()
894          *
895          * See_Also: https://issues.dlang.org/show_bug.cgi?id=18792
896          */
897         // static assert(!__traits(compiles, { string f1() @safe pure nothrow { immutable S x; return x[]; } }));
898     }
899 
900     // large will never allocate regardless of head-mutability
901     {
902         S s = S("123456789_123456");
903         assert(s.ptr is &s.opSlice()[0]);
904         assert(s.ptr is &s.toString()[0]); // shouldn't this change?
905     }
906 }
907 
908 @safe pure unittest
909 {
910     // TODO static immutable any = SSOString(`alpha`);
911 }
912 
913 ///
914 version(show)
915 @safe unittest
916 {
917     import std.stdio;
918     writeln(SSOString("alpha"));
919 }
920 
921 private enum isCharArray(T) = (is(T : const(char)[]));
922 
923 version(unittest)
924 {
925     import nxt.dip_traits : isDIP1000;
926 }