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 }