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 public struct Optional(T)
546 {
547     import std.typecons : Nullable;
548 
549     // workaround: types in union are not destructed
550     union DontCallDestructor { SafeUnqual!T t; }
551 
552     // workaround: types in struct are memcpied in move/moveEmplace, bypassing constness
553     struct UseMemcpyMove { DontCallDestructor u; }
554 
555     private UseMemcpyMove value = UseMemcpyMove.init;
556 
557     public bool isNull = true;
558 
559     public this(T value)
560     {
561         this.value = UseMemcpyMove(DontCallDestructor(value));
562         this.isNull = false;
563     }
564 
565     // This method should only be called from Builder.value! Builder fields are semantically write-only.
566     public inout(T) _get() inout
567     in
568     {
569         assert(!this.isNull);
570     }
571     do
572     {
573         return this.value.u.t;
574     }
575 
576     public U opAssign(U)(U value)
577     {
578         static if (is(U : Nullable!Arg, Arg))
579         {
580             import std.traits : Unqual;
581 
582             // fixup Nullable!(const T) -> Nullable!T
583             // Nullable!(const T) is a type that should not ever have been allowed to exist.
584             static if (is(T == Nullable!(Unqual!Arg)))
585             {
586                 static assert(is(Arg: Unqual!Arg), "Cannot assign Nullable!" ~ Arg.stringof ~ " to " ~ T.stringof);
587                 if (value.isNull)
588                 {
589                     _assign(T());
590                 }
591                 else
592                 {
593                     _assign(T(value.get));
594                 }
595             }
596             else
597             {
598                 // force-bypass the drug-fuelled `alias get this` idiocy by manually converting
599                 // value to the strictly (const) correct type for the assign() call
600                 T implConvertedValue = value;
601 
602                 _assign(implConvertedValue);
603             }
604         }
605         else
606         {
607             _assign(value);
608         }
609         return value;
610     }
611 
612     private void _assign(T value)
613     {
614         import std.algorithm : move, moveEmplace;
615 
616         auto valueCopy = UseMemcpyMove(DontCallDestructor(value));
617 
618         if (this.isNull)
619         {
620             moveEmplace(valueCopy, this.value);
621 
622             this.isNull = false;
623         }
624         else
625         {
626             move(valueCopy, this.value);
627         }
628     }
629 
630     public void opOpAssign(string op, RHS)(RHS rhs)
631     if (__traits(compiles, mixin("T.init " ~ op ~ " RHS.init")))
632     {
633         if (this.isNull)
634         {
635             this = T.init;
636         }
637         mixin("this = this._get " ~ op ~ " rhs;");
638     }
639 
640     static if (is(T: Nullable!Arg, Arg))
641     {
642         private void _assign(Arg value)
643         {
644             this = T(value);
645         }
646     }
647 
648     static if (is(T == struct) && hasElaborateDestructor!T)
649     {
650         ~this()
651         {
652             if (!this.isNull)
653             {
654                 destroy(this.value.u.t);
655             }
656         }
657     }
658 }
659 
660 ///
661 unittest
662 {
663     Optional!(int[]) intArrayOptional;
664 
665     assert(intArrayOptional.isNull);
666 
667     intArrayOptional ~= 5;
668 
669     assert(!intArrayOptional.isNull);
670     assert(intArrayOptional._get == [5]);
671 
672     intArrayOptional ~= 6;
673 
674     assert(intArrayOptional._get == [5, 6]);
675 }
676 
677 ///
678 @("optional correctly supports nullable assignment to const nullable of array")
679 unittest
680 {
681     import std.typecons : Nullable;
682 
683     Optional!(const(Nullable!int)) nullableArrayOptional;
684 
685     nullableArrayOptional = Nullable!int();
686 }
687 
688 private template SafeUnqual(T)
689 {
690     static if (__traits(compiles, (T t) { Unqual!T ut = t; }))
691     {
692         alias SafeUnqual = Unqual!T;
693     }
694     else
695     {
696         alias SafeUnqual = T;
697     }
698 }
699 
700 public string removeTrailingUnderline(string name)
701 {
702     import std..string : endsWith;
703 
704     return name.endsWith("_") ? name[0 .. $ - 1] : name;
705 }
706 
707 // Remove trailing underline iff the result would not be a reserved identifier
708 public enum optionallyRemoveTrailingUnderline(string name) =
709     isReservedIdentifier!(name.removeTrailingUnderline) ? name : name.removeTrailingUnderline;
710 
711 private enum isReservedIdentifier(string identifier) = !__traits(compiles, mixin(format!q{({ int %s; })}(identifier)));
712 
713 static assert(isReservedIdentifier!"version");
714 static assert(!isReservedIdentifier!"bla");
715 
716 /**
717  * manually reimplement `move`, `moveEmplace` because the phobos implementation of
718  * `moveEmplace` is **really, really bad!** it forces a recursive template
719  * instantiation over every primitive field in a struct, causing template overflows
720  * during compilation.
721  *
722  * See:
723  *      Phobos bug https://issues.dlang.org/show_bug.cgi?id=19689
724  *      Phobos fix https://github.com/dlang/phobos/pull/6873
725  */
726 public void moveEmplace(T)(ref T source, ref T dest) @trusted
727 in (&dest !is &source)
728 {
729     import core.stdc..string : memcpy, memset;
730 
731     memcpy(&dest, &source, T.sizeof);
732     memset(&source, 0, T.sizeof);
733 }
734 
735 ///
736 public void move(T)(ref T source, ref T dest) @trusted
737 in (&dest !is &source)
738 {
739     import std.traits : hasElaborateDestructor;
740 
741     static if (hasElaborateDestructor!T)
742     {
743         dest.__xdtor();
744     }
745     moveEmplace(source, dest);
746 }