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         return;
286     }
287     else
288     {
289         auto replaceArg(int i)()
290         if (i >= 0 && i < T.length)
291         {
292             alias PlainT = typeof(cast() args[i]);
293 
294             static if (is(PlainT == SysTime))
295             {
296                 static struct SysTimeInitWrapper
297                 {
298                     const typeof(args[i]) arg;
299 
300                     void toString(scope void delegate(const(char)[]) sink) const
301                     {
302                         if (this.arg is SysTime.init) // crashes on toString
303                         {
304                             sink("SysTime.init");
305                         }
306                         else
307                         {
308                             wrapFormatType(this.arg, false).toString(sink);
309                         }
310                     }
311                 }
312 
313                 return SysTimeInitWrapper(args[i]);
314             }
315             else
316             {
317                 return wrapFormatType(args[i], escapeStrings);
318             }
319         }
320 
321         if (comma)
322         {
323             sink(", ");
324         }
325 
326         comma = true;
327 
328         mixin(`sink.formattedWrite(fmt, ` ~ T.length.iota.map!(i => format!"replaceArg!%s"(i)).join(", ") ~ `);`);
329     }
330 }
331 
332 private auto wrapFormatType(T)(T value, bool escapeStrings)
333 {
334     import std.traits : isSomeString;
335     import std.typecons : Nullable;
336 
337     // for Nullable types, we cannot distinguish between a custom handler that takes Nullable!Arg
338     // and one that takes Arg via alias get this. So handlers that take Nullable are impossible, since we
339     // need to handle it here to avoid crashes.
340     static if (is(T: Nullable!Arg, Arg))
341     {
342         static struct NullableWrapper
343         {
344             T value;
345 
346             bool escapeStrings;
347 
348             void toString(scope void delegate(const(char)[]) sink) const
349             {
350                 if (this.value.isNull)
351                 {
352                     sink("null");
353                 }
354                 else
355                 {
356                     wrapFormatType(this.value.get, escapeStrings).toString(sink);
357                 }
358             }
359         }
360         return NullableWrapper(value, escapeStrings);
361     }
362     else static if (__traits(compiles, customToString(value, (void delegate(const(char)[])).init)))
363     {
364         static struct CustomToStringWrapper
365         {
366             T value;
367 
368             void toString(scope void delegate(const(char)[]) sink) const
369             {
370                 customToString(this.value, sink);
371             }
372         }
373         return CustomToStringWrapper(value);
374     }
375     else static if (is(T : V[K], K, V))
376     {
377         static if (isOrderingComparable!K)
378         {
379             return orderedAssociativeArray(value);
380         }
381         else
382         {
383             import std.traits : fullyQualifiedName;
384 
385             // ansi escape codes. 0: reset, 1: bold, 93: bright yellow
386             pragma(msg, "\x1b[1;93mWarning\x1b[0m: Consistent ordering of type \x1b[1m" ~ T.stringof ~ "\x1b[0m " ~
387                 "on output cannot be guaranteed.");
388             pragma(msg, "         Please implement opCmp for \x1b[1m" ~ fullyQualifiedName!K ~ "\x1b[0m.");
389 
390             return value;
391         }
392     }
393     else static if (isSomeString!T)
394     {
395         static struct QuoteStringWrapper
396         {
397             T value;
398 
399             bool escapeStrings;
400 
401             void toString(scope void delegate(const(char)[]) sink) const
402             {
403                 import std.format : formattedWrite;
404                 import std.range : only;
405 
406                 if (escapeStrings)
407                 {
408                     sink.formattedWrite!"%(%s%)"(this.value.only);
409                 }
410                 else
411                 {
412                     sink.formattedWrite!"%s"(this.value);
413                 }
414             }
415         }
416 
417         return QuoteStringWrapper(value, escapeStrings);
418     }
419     else
420     {
421         return value;
422     }
423 }
424 
425 private auto orderedAssociativeArray(T : V[K], K, V)(T associativeArray)
426 {
427     static struct OrderedAssociativeArray
428     {
429         T associativeArray;
430 
431         public void toString(scope void delegate(const(char)[]) sink) const
432         {
433             import std.algorithm : sort;
434             sink("[");
435 
436             bool comma = false;
437 
438             foreach (key; this.associativeArray.keys.sort)
439             {
440                 sink.sinkWrite(comma, true, "%s: %s", key, this.associativeArray[key]);
441             }
442             sink("]");
443         }
444     }
445 
446     return OrderedAssociativeArray(associativeArray);
447 }
448 
449 private string quote(string text)
450 {
451     import std.string : replace;
452 
453     return `"` ~ text.replace(`\`, `\\`).replace(`"`, `\"`) ~ `"`;
454 }
455 
456 private string genFormatFunctionImpl(string text)
457 {
458     import std.algorithm : findSplit;
459     import std.exception : enforce;
460     import std.format : format;
461     import std.range : empty;
462     import std.string : join;
463 
464     string[] fragments;
465 
466     string remainder = text;
467 
468     while (true)
469     {
470         auto splitLeft = remainder.findSplit("%(");
471 
472         if (splitLeft[1].empty)
473         {
474             break;
475         }
476 
477         auto splitRight = splitLeft[2].findSplit(")");
478 
479         enforce(!splitRight[1].empty, format!"Closing paren not found in '%s'"(remainder));
480         remainder = splitRight[2];
481 
482         fragments ~= quote(splitLeft[0]);
483         fragments ~= splitRight[0];
484     }
485     fragments ~= quote(remainder);
486 
487     return `string values(T)(T arg)
488     {
489         with (arg)
490         {
491             return ` ~ fragments.join(" ~ ") ~ `;
492         }
493     }`;
494 }
495 
496 public template formatNamed(string text)
497 {
498     mixin(genFormatFunctionImpl(text));
499 }
500 
501 ///
502 @("formatNamed replaces named keys with given values")
503 unittest
504 {
505     import std.typecons : tuple;
506     import unit_threaded.should : shouldEqual;
507 
508     formatNamed!("Hello %(second) World %(first)%(second)!")
509         .values(tuple!("first", "second")("3", "5"))
510         .shouldEqual("Hello 5 World 35!");
511 }
512 
513 public T[] reorder(T)(T[] source, size_t[] newOrder)
514 in
515 {
516     import std.algorithm : sort;
517     import std.range : array, iota;
518 
519     // newOrder must be a permutation of source indices
520     assert(newOrder.dup.sort.array == source.length.iota.array);
521 }
522 body
523 {
524     import std.algorithm : map;
525     import std.range : array;
526 
527     return newOrder.map!(i => source[i]).array;
528 }
529 
530 @("reorder returns reordered array")
531 unittest
532 {
533     import unit_threaded.should : shouldEqual;
534 
535     [1, 2, 3].reorder([0, 2, 1]).shouldEqual([1, 3, 2]);
536 }
537 
538 // TODO replace with Nullable once pr 19037 is merged
539 public struct Optional(T)
540 {
541     import std.typecons : Nullable;
542 
543     // workaround: types in union are not destructed
544     union DontCallDestructor { SafeUnqual!T t; }
545 
546     // workaround: types in struct are memcpied in move/moveEmplace, bypassing constness
547     struct UseMemcpyMove { DontCallDestructor u; }
548 
549     private UseMemcpyMove value = UseMemcpyMove.init;
550 
551     public bool isNull = true;
552 
553     public this(T value)
554     {
555         this.value = UseMemcpyMove(DontCallDestructor(value));
556         this.isNull = false;
557     }
558 
559     // This method should only be called from Builder.value! Builder fields are semantically write-only.
560     public inout(T) _get() inout
561     in
562     {
563         assert(!this.isNull);
564     }
565     do
566     {
567         return this.value.u.t;
568     }
569 
570     public void opAssign(U)(U value)
571     {
572         static if (is(U : Nullable!Arg, Arg))
573         {
574             // force-bypass the drug-fuelled `alias get this` idiocy by manually converting
575             // value to the strictly (const) correct type for the assign() call
576             T implConvertedValue = value;
577 
578             _assign(implConvertedValue);
579         }
580         else
581         {
582             _assign(value);
583         }
584     }
585 
586     private void _assign(T value)
587     {
588         import std.algorithm : move, moveEmplace;
589 
590         auto valueCopy = UseMemcpyMove(DontCallDestructor(value));
591 
592         if (this.isNull)
593         {
594             moveEmplace(valueCopy, this.value);
595 
596             this.isNull = false;
597         }
598         else
599         {
600             move(valueCopy, this.value);
601         }
602     }
603 
604     public void opOpAssign(string op, RHS)(RHS rhs)
605     if (__traits(compiles, mixin("T.init " ~ op ~ " RHS.init")))
606     {
607         if (this.isNull)
608         {
609             this = T.init;
610         }
611         mixin("this = this._get " ~ op ~ " rhs;");
612     }
613 
614     static if (is(T: Nullable!Arg, Arg))
615     {
616         private void _assign(Arg value)
617         {
618             this = T(value);
619         }
620     }
621 
622     static if (is(T == struct) && hasElaborateDestructor!T)
623     {
624         ~this()
625         {
626             if (!this.isNull)
627             {
628                 destroy(this.value.u.t);
629             }
630         }
631     }
632 }
633 
634 ///
635 unittest
636 {
637     Optional!(int[]) intArrayOptional;
638 
639     assert(intArrayOptional.isNull);
640 
641     intArrayOptional ~= 5;
642 
643     assert(!intArrayOptional.isNull);
644     assert(intArrayOptional._get == [5]);
645 
646     intArrayOptional ~= 6;
647 
648     assert(intArrayOptional._get == [5, 6]);
649 }
650 
651 ///
652 @("optional correctly supports nullable assignment to const nullable of array")
653 unittest
654 {
655     import std.typecons : Nullable;
656 
657     Optional!(const(Nullable!int)) nullableArrayOptional;
658 
659     nullableArrayOptional = Nullable!int();
660 }
661 
662 private template SafeUnqual(T)
663 {
664     static if (__traits(compiles, (T t) { Unqual!T ut = t; }))
665     {
666         alias SafeUnqual = Unqual!T;
667     }
668     else
669     {
670         alias SafeUnqual = T;
671     }
672 }
673 
674 public string removeTrailingUnderline(string name)
675 {
676     import std.string : endsWith;
677 
678     return name.endsWith("_") ? name[0 .. $ - 1] : name;
679 }
680 
681 /**
682  * manually reimplement `move`, `moveEmplace` because the phobos implementation of
683  * `moveEmplace` is **really, really bad!** it forces a recursive template
684  * instantiation over every primitive field in a struct, causing template overflows
685  * during compilation.
686  *
687  * See:
688  *      Phobos bug https://issues.dlang.org/show_bug.cgi?id=19689
689  *      Phobos fix https://github.com/dlang/phobos/pull/6873
690  */
691 public void moveEmplace(T)(ref T source, ref T dest) @trusted
692 in (&dest !is &source)
693 {
694     import core.stdc.string : memcpy, memset;
695 
696     memcpy(&dest, &source, T.sizeof);
697     memset(&source, 0, T.sizeof);
698 }
699 
700 ///
701 public void move(T)(ref T source, ref T dest) @trusted
702 in (&dest !is &source)
703 {
704     import std.traits : hasElaborateDestructor;
705 
706     static if (hasElaborateDestructor!T)
707     {
708         dest.__xdtor();
709     }
710     moveEmplace(source, dest);
711 }