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