1 module boilerplate.util; 2 3 import std.format; 4 import std.meta; 5 import std.range : iota; 6 import std.traits; 7 8 static if (__traits(compiles, { import config.string : toString; })) 9 { 10 import config.string : customToString = toString; 11 } 12 else 13 { 14 private void customToString(T)() 15 if (false) 16 { 17 } 18 } 19 20 enum needToDup(T) = isArray!(T) && !DeepConst!(T); 21 22 enum DeepConst(T) = __traits(compiles, (const T x) { T y = x; }); 23 24 @("needToDup correctly handles common types") 25 @nogc nothrow pure @safe unittest 26 { 27 int integerField; 28 int[] integerArrayField; 29 30 static assert(!needToDup!(typeof(integerField))); 31 static assert(needToDup!(typeof(integerArrayField))); 32 } 33 34 @("needToDup correctly handles const types") 35 @nogc nothrow pure @safe unittest 36 { 37 const(int)[] constIntegerArrayField; 38 string stringField; 39 40 static assert(!needToDup!(typeof(constIntegerArrayField))); 41 static assert(!needToDup!(typeof(stringField))); 42 } 43 44 @("doesn't add write-only properties to NormalMembers") 45 unittest 46 { 47 struct Test 48 { 49 @property void foo(int i) { } 50 mixin GenNormalMemberTuple; 51 static assert(is(NormalMemberTuple == AliasSeq!()), 52 "write-only properties should not appear in NormalMembers because they have no type" 53 ); 54 } 55 } 56 57 @("doesn't add read properties to NormalMembers if includeFunctions is false") 58 unittest 59 { 60 struct Test 61 { 62 @property int foo() { return 0; } 63 int bar() { return 0; } 64 mixin GenNormalMemberTuple; 65 static assert(is(NormalMemberTuple == AliasSeq!()), 66 "read properties should not appear in NormalMembers if includeFunctions is false" 67 ); 68 } 69 } 70 71 /** 72 * Generate AliasSeq of "normal" members - ie. no templates, no alias, no enum, only fields 73 * (and functions if includeFunctions is true). 74 */ 75 mixin template GenNormalMemberTuple(bool includeFunctions = false) 76 { 77 import boilerplate.util : GenNormalMembersCheck, GenNormalMembersImpl; 78 import std.meta : AliasSeq; 79 80 mixin(`alias NormalMemberTuple = ` ~ GenNormalMembersImpl([__traits(derivedMembers, typeof(this))], 81 mixin(GenNormalMembersCheck([__traits(derivedMembers, typeof(this))], includeFunctions))) ~ `;`); 82 } 83 84 string GenNormalMembersCheck(string[] members, bool includeFunctions) 85 { 86 import std.format : format; 87 import std.string : join; 88 89 string code = "["; 90 foreach (i, member; members) 91 { 92 if (i > 0) 93 { 94 code ~= ", "; // don't .map.join because this is compile performance critical code 95 } 96 97 if (member != "this") 98 { 99 string check = `__traits(compiles, &typeof(this).init.` ~ member ~ `)` 100 ~ ` && __traits(compiles, typeof(typeof(this).init.` ~ member ~ `))`; 101 102 if (!includeFunctions) 103 { 104 check ~= ` && !is(typeof(typeof(this).` ~ member ~ `) == function)` 105 ~ ` && !is(typeof(&typeof(this).init.` ~ member ~ `) == delegate)`; 106 } 107 108 code ~= check; 109 } 110 else 111 { 112 code ~= `false`; 113 } 114 } 115 code ~= "]"; 116 117 return code; 118 } 119 120 string GenNormalMembersImpl(string[] members, bool[] compiles) 121 { 122 import std.string : join; 123 124 string[] names; 125 126 foreach (i, member; members) 127 { 128 if (member != "this" && compiles[i]) 129 { 130 names ~= "\"" ~ member ~ "\""; 131 } 132 } 133 134 return "AliasSeq!(" ~ names.join(", ") ~ ")"; 135 } 136 137 template getOverloadLike(Aggregate, string Name, Type) 138 { 139 alias Overloads = AliasSeq!(__traits(getOverloads, Aggregate, Name)); 140 enum FunctionMatchesType(alias Fun) = is(typeof(Fun) == Type); 141 alias MatchingOverloads = Filter!(FunctionMatchesType, Overloads); 142 143 static assert(MatchingOverloads.length == 1); 144 145 alias getOverloadLike = MatchingOverloads[0]; 146 } 147 148 template udaIndex(alias attr, attributes...) 149 { 150 enum udaIndex = helper(); 151 152 ptrdiff_t helper() 153 { 154 if (!__ctfe) 155 { 156 return 0; 157 } 158 static if (attributes.length) 159 { 160 foreach (i, attrib; attributes) 161 { 162 enum lastAttrib = i + 1 == attributes.length; 163 164 static if (__traits(isTemplate, attr)) 165 { 166 static if (__traits(isSame, attrib, attr)) 167 { 168 return i; 169 } 170 else static if (is(attrib: attr!Args, Args...)) 171 { 172 return i; 173 } 174 else static if (lastAttrib) 175 { 176 return -1; 177 } 178 } 179 else static if (__traits(compiles, is(typeof(attrib) == typeof(attr)) && attrib == attr)) 180 { 181 static if (is(typeof(attrib) == typeof(attr)) && attrib == attr) 182 { 183 return i; 184 } 185 else static if (lastAttrib) 186 { 187 return -1; 188 } 189 } 190 else static if (__traits(compiles, typeof(attrib)) && __traits(compiles, is(typeof(attrib) == attr))) 191 { 192 static if (is(typeof(attrib) == attr)) 193 { 194 return i; 195 } 196 else static if (lastAttrib) 197 { 198 return -1; 199 } 200 } 201 else static if (__traits(compiles, is(attrib == attr))) 202 { 203 static if (is(attrib == attr)) 204 { 205 return i; 206 } 207 else static if (lastAttrib) 208 { 209 return -1; 210 } 211 } 212 else static if (lastAttrib) 213 { 214 return -1; 215 } 216 } 217 } 218 else 219 { 220 return -1; 221 } 222 } 223 } 224 225 string isStatic(string field) 226 { 227 return `__traits(getOverloads, typeof(this), "` ~ field ~ `").length == 0` 228 ~ ` && __traits(compiles, &this.` ~ field ~ `)`; 229 } 230 231 string isUnsafe(string field) 232 { 233 return isStatic(field) ~ ` && !__traits(compiles, () @safe { return this.` ~ field ~ `; })`; 234 } 235 236 // a stable, simple O(n) sort optimal for a small number of sort keys 237 T[] bucketSort(T)(T[] inputArray, size_t delegate(T) rankfn) 238 { 239 import std.algorithm : joiner; 240 import std.range : array; 241 242 T[][] buckets; 243 244 foreach (element; inputArray) 245 { 246 auto rank = rankfn(element); 247 248 if (rank >= buckets.length) 249 { 250 buckets.length = rank + 1; 251 } 252 253 buckets[rank] ~= element; 254 } 255 256 return buckets.joiner.array; 257 } 258 259 void sinkWrite(T...)(scope void delegate(const(char)[]) sink, ref bool comma, bool escapeStrings, string fmt, T args) 260 { 261 import std.algorithm : map; 262 import std.datetime : SysTime; 263 import std.format : format, formattedWrite; 264 import std.string : join; 265 import std.typecons : Nullable; 266 267 static if (T.length == 1) // check for optional field: single Nullable 268 { 269 const arg = args[0]; 270 271 alias PlainT = typeof(cast() arg); 272 273 enum isNullable = is(PlainT: Nullable!Arg, Arg); 274 } 275 else 276 { 277 enum isNullable = false; 278 } 279 280 static if (isNullable) 281 { 282 if (!arg.isNull) 283 { 284 sink.sinkWrite(comma, escapeStrings, fmt, arg.get); 285 } 286 else 287 { 288 sink.sinkWrite(comma, false, fmt, "Nullable.null"); 289 } 290 return; 291 } 292 else 293 { 294 auto replaceArg(int i)() 295 if (i >= 0 && i < T.length) 296 { 297 alias PlainT = typeof(cast() args[i]); 298 299 static if (is(PlainT == SysTime)) 300 { 301 static struct SysTimeInitWrapper 302 { 303 const typeof(args[i]) arg; 304 305 void toString(scope void delegate(const(char)[]) sink) const 306 { 307 if (this.arg is SysTime.init) // crashes on toString 308 { 309 sink("SysTime.init"); 310 } 311 else 312 { 313 wrapFormatType(this.arg, false).toString(sink); 314 } 315 } 316 } 317 318 return SysTimeInitWrapper(args[i]); 319 } 320 else 321 { 322 return wrapFormatType(args[i], escapeStrings); 323 } 324 } 325 326 if (comma) 327 { 328 sink(", "); 329 } 330 331 comma = true; 332 333 mixin(`sink.formattedWrite(fmt, ` ~ T.length.iota.map!(i => format!"replaceArg!%s"(i)).join(", ") ~ `);`); 334 } 335 } 336 337 private auto wrapFormatType(T)(T value, bool escapeStrings) 338 { 339 import std.traits : isSomeString; 340 import std.typecons : Nullable; 341 342 // for Nullable types, we cannot distinguish between a custom handler that takes Nullable!Arg 343 // and one that takes Arg via alias get this. So handlers that take Nullable are impossible, since we 344 // need to handle it here to avoid crashes. 345 static if (is(T: Nullable!Arg, Arg)) 346 { 347 static struct NullableWrapper 348 { 349 T value; 350 351 bool escapeStrings; 352 353 void toString(scope void delegate(const(char)[]) sink) const 354 { 355 if (this.value.isNull) 356 { 357 sink("null"); 358 } 359 else 360 { 361 wrapFormatType(this.value.get, escapeStrings).toString(sink); 362 } 363 } 364 } 365 return NullableWrapper(value, escapeStrings); 366 } 367 else static if (__traits(compiles, customToString(value, (void delegate(const(char)[])).init))) 368 { 369 static struct CustomToStringWrapper 370 { 371 T value; 372 373 void toString(scope void delegate(const(char)[]) sink) const 374 { 375 customToString(this.value, sink); 376 } 377 } 378 return CustomToStringWrapper(value); 379 } 380 else static if (is(T : V[K], K, V)) 381 { 382 static if (isOrderingComparable!K) 383 { 384 return orderedAssociativeArray(value); 385 } 386 else 387 { 388 import std.traits : fullyQualifiedName; 389 390 // ansi escape codes. 0: reset, 1: bold, 93: bright yellow 391 pragma(msg, "\x1b[1;93mWarning\x1b[0m: Consistent ordering of type \x1b[1m" ~ T.stringof ~ "\x1b[0m " ~ 392 "on output cannot be guaranteed."); 393 pragma(msg, " Please implement opCmp for \x1b[1m" ~ fullyQualifiedName!K ~ "\x1b[0m."); 394 395 return value; 396 } 397 } 398 else static if (isSomeString!T) 399 { 400 static struct QuoteStringWrapper 401 { 402 T value; 403 404 bool escapeStrings; 405 406 void toString(scope void delegate(const(char)[]) sink) const 407 { 408 import std.format : formattedWrite; 409 import std.range : only; 410 411 if (escapeStrings) 412 { 413 sink.formattedWrite!"%(%s%)"(this.value.only); 414 } 415 else 416 { 417 sink.formattedWrite!"%s"(this.value); 418 } 419 } 420 } 421 422 return QuoteStringWrapper(value, escapeStrings); 423 } 424 else 425 { 426 return value; 427 } 428 } 429 430 private auto orderedAssociativeArray(T : V[K], K, V)(T associativeArray) 431 { 432 static struct OrderedAssociativeArray 433 { 434 T associativeArray; 435 436 public void toString(scope void delegate(const(char)[]) sink) const 437 { 438 import std.algorithm : sort; 439 sink("["); 440 441 bool comma = false; 442 443 foreach (key; this.associativeArray.keys.sort) 444 { 445 sink.sinkWrite(comma, true, "%s: %s", key, this.associativeArray[key]); 446 } 447 sink("]"); 448 } 449 } 450 451 return OrderedAssociativeArray(associativeArray); 452 } 453 454 private string quote(string text) 455 { 456 import std.string : replace; 457 458 return `"` ~ text.replace(`\`, `\\`).replace(`"`, `\"`) ~ `"`; 459 } 460 461 private string genFormatFunctionImpl(string text) 462 { 463 import std.algorithm : findSplit; 464 import std.exception : enforce; 465 import std.format : format; 466 import std.range : empty; 467 import std.string : join; 468 469 string[] fragments; 470 471 string remainder = text; 472 473 while (true) 474 { 475 auto splitLeft = remainder.findSplit("%("); 476 477 if (splitLeft[1].empty) 478 { 479 break; 480 } 481 482 auto splitRight = splitLeft[2].findSplit(")"); 483 484 enforce(!splitRight[1].empty, format!"Closing paren not found in '%s'"(remainder)); 485 remainder = splitRight[2]; 486 487 fragments ~= quote(splitLeft[0]); 488 fragments ~= splitRight[0]; 489 } 490 fragments ~= quote(remainder); 491 492 return `string values(T)(T arg) 493 { 494 with (arg) 495 { 496 return ` ~ fragments.join(" ~ ") ~ `; 497 } 498 }`; 499 } 500 501 public template formatNamed(string text) 502 { 503 mixin(genFormatFunctionImpl(text)); 504 } 505 506 /// 507 @("formatNamed replaces named keys with given values") 508 unittest 509 { 510 import std.typecons : tuple; 511 import unit_threaded.should : shouldEqual; 512 513 formatNamed!("Hello %(second) World %(first)%(second)!") 514 .values(tuple!("first", "second")("3", "5")) 515 .shouldEqual("Hello 5 World 35!"); 516 } 517 518 public T[] reorder(T)(T[] source, size_t[] newOrder) 519 in 520 { 521 import std.algorithm : sort; 522 import std.range : array, iota; 523 524 // newOrder must be a permutation of source indices 525 assert(newOrder.dup.sort.array == source.length.iota.array); 526 } 527 body 528 { 529 import std.algorithm : map; 530 import std.range : array; 531 532 return newOrder.map!(i => source[i]).array; 533 } 534 535 @("reorder returns reordered array") 536 unittest 537 { 538 import unit_threaded.should : shouldEqual; 539 540 [1, 2, 3].reorder([0, 2, 1]).shouldEqual([1, 3, 2]); 541 } 542 543 // TODO replace with Nullable once pr 19037 is merged 544 public struct Optional(T) 545 { 546 import std.typecons : Nullable; 547 548 // workaround: types in union are not destructed 549 union DontCallDestructor { SafeUnqual!T t; } 550 551 // workaround: types in struct are memcpied in move/moveEmplace, bypassing constness 552 struct UseMemcpyMove { DontCallDestructor u; } 553 554 private UseMemcpyMove value = UseMemcpyMove.init; 555 556 public bool isNull = true; 557 558 public this(T value) 559 { 560 this.value = UseMemcpyMove(DontCallDestructor(value)); 561 this.isNull = false; 562 } 563 564 // This method should only be called from Builder.value! Builder fields are semantically write-only. 565 public inout(T) _get() inout 566 in 567 { 568 assert(!this.isNull); 569 } 570 do 571 { 572 return this.value.u.t; 573 } 574 575 public U opAssign(U)(U value) 576 { 577 static if (is(U : Nullable!Arg, Arg)) 578 { 579 // force-bypass the drug-fuelled `alias get this` idiocy by manually converting 580 // value to the strictly (const) correct type for the assign() call 581 T implConvertedValue = value; 582 583 _assign(implConvertedValue); 584 } 585 else 586 { 587 _assign(value); 588 } 589 return value; 590 } 591 592 private void _assign(T value) 593 { 594 import std.algorithm : move, moveEmplace; 595 596 auto valueCopy = UseMemcpyMove(DontCallDestructor(value)); 597 598 if (this.isNull) 599 { 600 moveEmplace(valueCopy, this.value); 601 602 this.isNull = false; 603 } 604 else 605 { 606 move(valueCopy, this.value); 607 } 608 } 609 610 public void opOpAssign(string op, RHS)(RHS rhs) 611 if (__traits(compiles, mixin("T.init " ~ op ~ " RHS.init"))) 612 { 613 if (this.isNull) 614 { 615 this = T.init; 616 } 617 mixin("this = this._get " ~ op ~ " rhs;"); 618 } 619 620 static if (is(T: Nullable!Arg, Arg)) 621 { 622 private void _assign(Arg value) 623 { 624 this = T(value); 625 } 626 } 627 628 static if (is(T == struct) && hasElaborateDestructor!T) 629 { 630 ~this() 631 { 632 if (!this.isNull) 633 { 634 destroy(this.value.u.t); 635 } 636 } 637 } 638 } 639 640 /// 641 unittest 642 { 643 Optional!(int[]) intArrayOptional; 644 645 assert(intArrayOptional.isNull); 646 647 intArrayOptional ~= 5; 648 649 assert(!intArrayOptional.isNull); 650 assert(intArrayOptional._get == [5]); 651 652 intArrayOptional ~= 6; 653 654 assert(intArrayOptional._get == [5, 6]); 655 } 656 657 /// 658 @("optional correctly supports nullable assignment to const nullable of array") 659 unittest 660 { 661 import std.typecons : Nullable; 662 663 Optional!(const(Nullable!int)) nullableArrayOptional; 664 665 nullableArrayOptional = Nullable!int(); 666 } 667 668 private template SafeUnqual(T) 669 { 670 static if (__traits(compiles, (T t) { Unqual!T ut = t; })) 671 { 672 alias SafeUnqual = Unqual!T; 673 } 674 else 675 { 676 alias SafeUnqual = T; 677 } 678 } 679 680 public string removeTrailingUnderline(string name) 681 { 682 import std.string : endsWith; 683 684 return name.endsWith("_") ? name[0 .. $ - 1] : name; 685 } 686 687 // Remove trailing underline iff the result would not be a reserved identifier 688 public enum optionallyRemoveTrailingUnderline(string name) = 689 isReservedIdentifier!(name.removeTrailingUnderline) ? name : name.removeTrailingUnderline; 690 691 private enum isReservedIdentifier(string identifier) = !__traits(compiles, mixin(format!q{({ int %s; })}(identifier))); 692 693 static assert(isReservedIdentifier!"version"); 694 static assert(!isReservedIdentifier!"bla"); 695 696 /** 697 * manually reimplement `move`, `moveEmplace` because the phobos implementation of 698 * `moveEmplace` is **really, really bad!** it forces a recursive template 699 * instantiation over every primitive field in a struct, causing template overflows 700 * during compilation. 701 * 702 * See: 703 * Phobos bug https://issues.dlang.org/show_bug.cgi?id=19689 704 * Phobos fix https://github.com/dlang/phobos/pull/6873 705 */ 706 public void moveEmplace(T)(ref T source, ref T dest) @trusted 707 in (&dest !is &source) 708 { 709 import core.stdc.string : memcpy, memset; 710 711 memcpy(&dest, &source, T.sizeof); 712 memset(&source, 0, T.sizeof); 713 } 714 715 /// 716 public void move(T)(ref T source, ref T dest) @trusted 717 in (&dest !is &source) 718 { 719 import std.traits : hasElaborateDestructor; 720 721 static if (hasElaborateDestructor!T) 722 { 723 dest.__xdtor(); 724 } 725 moveEmplace(source, dest); 726 }