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 }