1 /*
2 * Support for rich error messages generation with `assert`
3 *
4 * This module provides the `_d_assert_fail` hooks which are instantiated
5 * by the compiler whenever `-checkaction=context` is used.
6 * There are two hooks, one for unary expressions, and one for binary.
7 * When used, the compiler will rewrite `assert(a >= b)` as
8 * `assert(a >= b, _d_assert_fail!(typeof(a))(">=", a, b))`.
9 * Temporaries will be created to avoid side effects if deemed necessary
10 * by the compiler.
11 *
12 * For more information, refer to the implementation in DMD frontend
13 * for `AssertExpression`'s semantic analysis.
14 *
15 * Copyright: D Language Foundation 2018 - 2020
16 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
17 * Source: $(LINK2 https://github.com/dlang/druntime/blob/master/src/core/internal/dassert.d, _dassert.d)
18 * Documentation: https://dlang.org/phobos/core_internal_dassert.html
19 */
20 module core.internal.dassert;
21
22 /**
23 * Generates rich assert error messages for unary expressions
24 *
25 * The unary expression `assert(!una)` will be turned into
26 * `assert(!una, _d_assert_fail("!", una))`.
27 * This routine simply acts as if the user wrote `assert(una == false)`.
28 *
29 * Params:
30 * op = Operator that was used in the expression, currently only "!"
31 * is supported.
32 * a = Result of the expression that was used in `assert` before
33 * its implicit conversion to `bool`.
34 *
35 * Returns:
36 * A string such as "$a != true" or "$a == true".
37 */
_d_assert_fail(A)38 string _d_assert_fail(A)(const scope string op, auto ref const scope A a)
39 {
40 // Prevent InvalidMemoryOperationError when triggered from a finalizer
41 if (inFinalizer())
42 return "Assertion failed (rich formatting is disabled in finalizers)";
43
44 string[2] vals = [ miniFormatFakeAttributes(a), "true" ];
45 immutable token = op == "!" ? "==" : "!=";
46 return combine(vals[0 .. 1], token, vals[1 .. $]);
47 }
48
49 /**
50 * Generates rich assert error messages for binary expressions
51 *
52 * The binary expression `assert(x == y)` will be turned into
53 * `assert(x == y, _d_assert_fail!(typeof(x))("==", x, y))`.
54 *
55 * Params:
56 * comp = Comparison operator that was used in the expression.
57 * a = Left hand side operand (can be a tuple).
58 * b = Right hand side operand (can be a tuple).
59 *
60 * Returns:
61 * A string such as "$a $comp $b".
62 */
_d_assert_fail(A...)63 template _d_assert_fail(A...)
64 {
65 string _d_assert_fail(B...)(
66 const scope string comp, auto ref const scope A a, auto ref const scope B b)
67 if (B.length != 0 || A.length != 1) // Resolve ambiguity with unary overload
68 {
69 // Prevent InvalidMemoryOperationError when triggered from a finalizer
70 if (inFinalizer())
71 return "Assertion failed (rich formatting is disabled in finalizers)";
72
73 string[A.length + B.length] vals;
74 static foreach (idx; 0 .. A.length)
75 vals[idx] = miniFormatFakeAttributes(a[idx]);
76 static foreach (idx; 0 .. B.length)
77 vals[A.length + idx] = miniFormatFakeAttributes(b[idx]);
78 immutable token = invertCompToken(comp);
79 return combine(vals[0 .. A.length], token, vals[A.length .. $]);
80 }
81 }
82
83 /// Combines the supplied arguments into one string `"valA token valB"`
combine(const scope string[]valA,const scope string token,const scope string[]valB)84 private string combine(const scope string[] valA, const scope string token,
85 const scope string[] valB) pure nothrow @nogc @safe
86 {
87 // Each separator is 2 chars (", "), plus the two spaces around the token.
88 size_t totalLen = (valA.length - 1) * 2 +
89 (valB.length - 1) * 2 + 2 + token.length;
90
91 // Empty arrays are printed as ()
92 if (valA.length == 0) totalLen += 2;
93 if (valB.length == 0) totalLen += 2;
94
95 foreach (v; valA) totalLen += v.length;
96 foreach (v; valB) totalLen += v.length;
97
98 // Include braces when printing tuples
99 const printBraces = (valA.length + valB.length) != 2;
100 if (printBraces) totalLen += 4; // '(', ')' for both tuples
101
102 char[] buffer = cast(char[]) pureAlloc(totalLen)[0 .. totalLen];
103 // @nogc-concat of "<valA> <comp> <valB>"
104 static void formatTuple (scope char[] buffer, ref size_t n, in string[] vals, in bool printBraces)
105 {
106 if (printBraces) buffer[n++] = '(';
107 foreach (idx, v; vals)
108 {
109 if (idx)
110 {
111 buffer[n++] = ',';
112 buffer[n++] = ' ';
113 }
114 buffer[n .. n + v.length] = v;
115 n += v.length;
116 }
117 if (printBraces) buffer[n++] = ')';
118 }
119
120 size_t n;
121 formatTuple(buffer, n, valA, printBraces);
122 buffer[n++] = ' ';
123 buffer[n .. n + token.length] = token;
124 n += token.length;
125 buffer[n++] = ' ';
126 formatTuple(buffer, n, valB, printBraces);
127 return (() @trusted => cast(string) buffer)();
128 }
129
130 /// Yields the appropriate `printf` format token for a type `T`
getPrintfFormat(T)131 private template getPrintfFormat(T)
132 {
133 static if (is(T == long))
134 {
135 enum getPrintfFormat = "%lld";
136 }
137 else static if (is(T == ulong))
138 {
139 enum getPrintfFormat = "%llu";
140 }
141 else static if (__traits(isIntegral, T))
142 {
143 static if (__traits(isUnsigned, T))
144 {
145 enum getPrintfFormat = "%u";
146 }
147 else
148 {
149 enum getPrintfFormat = "%d";
150 }
151 }
152 else
153 {
154 static assert(0, "Unknown format");
155 }
156 }
157
158 /**
159 * Generates a textual representation of `v` without relying on Phobos.
160 * The value is formatted as follows:
161 *
162 * - primitive types and arrays yield their respective literals
163 * - pointers are printed as hexadecimal numbers
164 * - enum members are represented by their name
165 * - user-defined types are formatted by either calling `toString`
166 * if defined or printing all members, e.g. `S(1, 2)`
167 *
168 * Note that unions are rejected because this method cannot determine which
169 * member is valid when calling this method.
170 *
171 * Params:
172 * v = the value to print
173 *
174 * Returns: a string respresenting `v` or `V.stringof` if `V` is not supported
175 */
miniFormat(V)176 private string miniFormat(V)(const scope ref V v)
177 {
178 import core.internal.traits: isAggregateType;
179
180 /// `shared` values are formatted as their base type
181 static if (is(V == shared T, T))
182 {
183 // Use atomics to avoid race conditions whenever possible
184 static if (__traits(compiles, atomicLoad(v)))
185 {
186 if (!__ctfe)
187 {
188 T tmp = cast(T) atomicLoad(v);
189 return miniFormat(tmp);
190 }
191 }
192
193 // Fall back to a simple cast - we're violating the type system anyways
194 return miniFormat(*cast(const T*) &v);
195 }
196 // Format enum members using their name
197 else static if (is(V BaseType == enum))
198 {
199 // Always generate repeated if's instead of switch to skip the detection
200 // of non-integral enums. This method doesn't need to be fast.
201 static foreach (mem; __traits(allMembers, V))
202 {
203 if (v == __traits(getMember, V, mem))
204 return mem;
205 }
206
207 // Format invalid enum values as their base type
208 enum cast_ = "cast(" ~ V.stringof ~ ")";
209 const val = miniFormat(__ctfe ? cast(const BaseType) v : *cast(const BaseType*) &v);
210 return combine([ cast_ ], "", [ val ]);
211 }
212 else static if (is(V == bool))
213 {
214 return v ? "true" : "false";
215 }
216 // Detect vectors which match isIntegral / isFloating
217 else static if (is(V == __vector(ET[N]), ET, size_t N))
218 {
219 string msg = "[";
220 foreach (i; 0 .. N)
221 {
222 if (i > 0)
223 msg ~= ", ";
224
225 msg ~= miniFormat(v[i]);
226 }
227 msg ~= "]";
228 return msg;
229 }
230 else static if (__traits(isIntegral, V))
231 {
232 static if (is(V == char))
233 {
234 // Avoid invalid code points
235 if (v < 0x7F)
236 return ['\'', v, '\''];
237
238 uint tmp = v;
239 return "cast(char) " ~ miniFormat(tmp);
240 }
241 else static if (is(V == wchar) || is(V == dchar))
242 {
243 import core.internal.utf: isValidDchar, toUTF8;
244
245 // Avoid invalid code points
246 if (isValidDchar(v))
247 return toUTF8(['\'', v, '\'']);
248
249 uint tmp = v;
250 return "cast(" ~ V.stringof ~ ") " ~ miniFormat(tmp);
251 }
252 else
253 {
254 import core.internal.string;
255 static if (__traits(isUnsigned, V))
256 const val = unsignedToTempString(v);
257 else
258 const val = signedToTempString(v);
259 return val.get().idup();
260 }
261 }
262 else static if (__traits(isFloating, V))
263 {
264 import core.stdc.stdio : sprintf;
265 import core.stdc.config : LD = c_long_double;
266
267 // No suitable replacement for sprintf in druntime ATM
268 if (__ctfe)
269 return '<' ~ V.stringof ~ " not supported>";
270
271 // Workaround for https://issues.dlang.org/show_bug.cgi?id=20759
272 static if (is(LD == real))
273 enum realFmt = "%Lg";
274 else
275 enum realFmt = "%g";
276
277 char[60] val;
278 int len;
279 static if (is(V == float) || is(V == double))
280 len = sprintf(&val[0], "%g", v);
281 else static if (is(V == real))
282 len = sprintf(&val[0], realFmt, cast(LD) v);
283 else static if (is(V == cfloat) || is(V == cdouble))
284 len = sprintf(&val[0], "%g + %gi", v.re, v.im);
285 else static if (is(V == creal))
286 len = sprintf(&val[0], realFmt ~ " + " ~ realFmt ~ 'i', cast(LD) v.re, cast(LD) v.im);
287 else static if (is(V == ifloat) || is(V == idouble))
288 len = sprintf(&val[0], "%gi", v);
289 else // ireal
290 {
291 static assert(is(V == ireal));
292 static if (is(LD == real))
293 alias R = ireal;
294 else
295 alias R = idouble;
296 len = sprintf(&val[0], realFmt ~ 'i', cast(R) v);
297 }
298 return val.idup[0 .. len];
299 }
300 // special-handling for void-arrays
301 else static if (is(V == typeof(null)))
302 {
303 return "`null`";
304 }
305 else static if (is(V == U*, U))
306 {
307 // Format as ulong and prepend a 0x for pointers
308 import core.internal.string;
309 return cast(immutable) ("0x" ~ unsignedToTempString!16(cast(ulong) v));
310 }
311 // toString() isn't always const, e.g. classes inheriting from Object
312 else static if (__traits(compiles, { string s = V.init.toString(); }))
313 {
314 // Object references / struct pointers may be null
315 static if (is(V == class) || is(V == interface))
316 {
317 if (v is null)
318 return "`null`";
319 }
320
321 try
322 {
323 // Prefer const overload of toString
324 static if (__traits(compiles, { string s = v.toString(); }))
325 return v.toString();
326 else
327 return (cast() v).toString();
328 }
329 catch (Exception e)
330 {
331 return `<toString() failed: "` ~ e.msg ~ `", called on ` ~ formatMembers(v) ~`>`;
332 }
333 }
334 // Static arrays or slices (but not aggregates with `alias this`)
335 else static if (is(V : U[], U) && !isAggregateType!V)
336 {
337 import core.internal.traits: Unqual;
338 alias E = Unqual!U;
339
340 // special-handling for void-arrays
341 static if (is(E == void))
342 {
343 if (__ctfe)
344 return "<void[] not supported>";
345
346 const bytes = cast(byte[]) v;
347 return miniFormat(bytes);
348 }
349 // anything string-like
350 else static if (is(E == char) || is(E == dchar) || is(E == wchar))
351 {
352 const s = `"` ~ v ~ `"`;
353
354 // v could be a char[], dchar[] or wchar[]
355 static if (is(typeof(s) : const char[]))
356 return cast(immutable) s;
357 else
358 {
359 import core.internal.utf: toUTF8;
360 return toUTF8(s);
361 }
362 }
363 else
364 {
365 string msg = "[";
366 foreach (i, ref el; v)
367 {
368 if (i > 0)
369 msg ~= ", ";
370
371 // don't fully print big arrays
372 if (i >= 30)
373 {
374 msg ~= "...";
375 break;
376 }
377 msg ~= miniFormat(el);
378 }
379 msg ~= "]";
380 return msg;
381 }
382 }
383 else static if (is(V : Val[K], K, Val))
384 {
385 size_t i;
386 string msg = "[";
387 foreach (ref k, ref val; v)
388 {
389 if (i > 0)
390 msg ~= ", ";
391 // don't fully print big AAs
392 if (i++ >= 30)
393 {
394 msg ~= "...";
395 break;
396 }
397 msg ~= miniFormat(k) ~ ": " ~ miniFormat(val);
398 }
399 msg ~= "]";
400 return msg;
401 }
402 else static if (is(V == struct))
403 {
404 return formatMembers(v);
405 }
406 // Extern C++ classes don't have a toString by default
407 else static if (is(V == class) || is(V == interface))
408 {
409 if (v is null)
410 return "null";
411
412 // Extern classes might be opaque
413 static if (is(typeof(v.tupleof)))
414 return formatMembers(v);
415 else
416 return '<' ~ V.stringof ~ '>';
417 }
418 else
419 {
420 return V.stringof;
421 }
422 }
423
424 /// Formats `v`'s members as `V(<member 1>, <member 2>, ...)`
formatMembers(V)425 private string formatMembers(V)(const scope ref V v)
426 {
427 enum ctxPtr = __traits(isNested, V);
428 enum isOverlapped = calcFieldOverlap([ v.tupleof.offsetof ]);
429
430 string msg = V.stringof ~ "(";
431 foreach (i, ref field; v.tupleof)
432 {
433 if (i > 0)
434 msg ~= ", ";
435
436 static if (isOverlapped[i])
437 {
438 msg ~= "<overlapped field>";
439 }
440 else
441 {
442 // Mark context pointer
443 static if (ctxPtr && i == v.tupleof.length - 1)
444 msg ~= "<context>: ";
445
446 msg ~= miniFormat(field);
447 }
448 }
449 msg ~= ")";
450 return msg;
451 }
452
453 /**
454 * Calculates whether fields are overlapped based on the passed offsets.
455 *
456 * Params:
457 * offsets = offsets of all fields matching the order of `.tupleof`
458 *
459 * Returns: an array such that arr[n] = true indicates that the n'th field
460 * overlaps with an adjacent field
461 **/
calcFieldOverlap(const scope size_t[]offsets)462 private bool[] calcFieldOverlap(const scope size_t[] offsets)
463 {
464 bool[] overlaps = new bool[](offsets.length);
465
466 foreach (const idx; 1 .. overlaps.length)
467 {
468 if (offsets[idx - 1] == offsets[idx])
469 overlaps[idx - 1] = overlaps[idx] = true;
470 }
471
472 return overlaps;
473 }
474
475 // This should be a local import in miniFormat but fails with a cyclic dependency error
476 // core.thread.osthread -> core.time -> object -> core.internal.array.capacity
477 // -> core.atomic -> core.thread -> core.thread.osthread
478 import core.atomic : atomicLoad;
479
480 /// Negates a comparison token, e.g. `==` is mapped to `!=`
invertCompToken(scope string comp)481 private string invertCompToken(scope string comp) pure nothrow @nogc @safe
482 {
483 switch (comp)
484 {
485 case "==":
486 return "!=";
487 case "!=":
488 return "==";
489 case "<":
490 return ">=";
491 case "<=":
492 return ">";
493 case ">":
494 return "<=";
495 case ">=":
496 return "<";
497 case "is":
498 return "!is";
499 case "!is":
500 return "is";
501 case "in":
502 return "!in";
503 case "!in":
504 return "in";
505 default:
506 assert(0, combine(["Invalid comparison operator '"], comp, ["'"]));
507 }
508 }
509
510 /// Casts the function pointer to include `@safe`, `@nogc`, ...
assumeFakeAttributes(T)511 private auto assumeFakeAttributes(T)(T t) @trusted
512 {
513 import core.internal.traits : Parameters, ReturnType;
514 alias RT = ReturnType!T;
515 alias P = Parameters!T;
516 alias type = RT function(P) nothrow @nogc @safe pure;
517 return cast(type) t;
518 }
519
520 /// Wrapper for `miniFormat` which assumes that the implementation is `@safe`, `@nogc`, ...
521 /// s.t. it does not violate the constraints of the the function containing the `assert`.
miniFormatFakeAttributes(T)522 private string miniFormatFakeAttributes(T)(const scope ref T t)
523 {
524 alias miniT = miniFormat!T;
525 return assumeFakeAttributes(&miniT)(t);
526 }
527
528 /// Allocates an array of `t` bytes while pretending to be `@safe`, `@nogc`, ...
pureAlloc(size_t t)529 private auto pureAlloc(size_t t)
530 {
531 static auto alloc(size_t len)
532 {
533 return new ubyte[len];
534 }
535 return assumeFakeAttributes(&alloc)(t);
536 }
537
538 /// Wrapper for GC.inFinalizer that fakes purity
inFinalizer()539 private bool inFinalizer()() pure nothrow @nogc @safe
540 {
541 // CTFE doesn't trigger InvalidMemoryErrors
542 import core.memory : GC;
543 return !__ctfe && assumeFakeAttributes(&GC.inFinalizer)();
544 }
545
546 // https://issues.dlang.org/show_bug.cgi?id=21544
547 unittest
548 {
549 // Normal enum values
550 enum E { A, BCDE }
551 E e = E.A;
552 assert(miniFormat(e) == "A");
553 e = E.BCDE;
554 assert(miniFormat(e) == "BCDE");
555
556 // Invalid enum value is printed as their implicit base type (int)
557 e = cast(E) 3;
558 assert(miniFormat(e) == "cast(E) 3");
559
560 // Non-integral enums work as well
561 static struct S
562 {
563 int a;
564 string str;
565 }
566
567 enum E2 : S { a2 = S(1, "Hello") }
568 E2 es = E2.a2;
569 assert(miniFormat(es) == `a2`);
570
571 // Even invalid values
572 es = cast(E2) S(2, "World");
573 assert(miniFormat(es) == `cast(E2) S(2, "World")`);
574 }
575
576 // vectors
577 unittest
578 {
579 static if (is(__vector(float[4])))
580 {
581 __vector(float[4]) f = [-1.5f, 0.5f, 1.0f, 0.125f];
582 assert(miniFormat(f) == "[-1.5, 0.5, 1, 0.125]");
583 }
584
585 static if (is(__vector(int[4])))
586 {
587 __vector(int[4]) i = [-1, 0, 1, 3];
588 assert(miniFormat(i) == "[-1, 0, 1, 3]");
589 }
590 }
591