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