2 * Support for rich error messages generation with `assert`
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
12 * For more information, refer to the implementation in DMD frontend
13 * for `AssertExpression`'s semantic analysis.
15 * Copyright: D Language Foundation 2018 - 2020
16 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
17 * Source: $(DRUNTIMESRC core/internal/_dassert.d)
18 * Documentation: https://dlang.org/phobos/core_internal_dassert.html
20 module core
.internal
.dassert
;
23 * Generates rich assert error messages for unary expressions
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)`.
30 * op = Operator that was used in the expression, currently only "!"
32 * a = Result of the expression that was used in `assert` before
33 * its implicit conversion to `bool`.
36 * A string such as "$a != true" or "$a == true".
38 string
_d_assert_fail(A
)(const scope string op
, auto ref const scope A a
)
40 // Prevent InvalidMemoryOperationError when triggered from a finalizer
42 return "Assertion failed (rich formatting is disabled in finalizers)";
44 string
[2] vals
= [ miniFormatFakeAttributes(a
), "true" ];
45 immutable token
= op
== "!" ?
"==" : "!=";
46 return combine(vals
[0 .. 1], token
, vals
[1 .. $]);
50 * Generates rich assert error messages for binary expressions
52 * The binary expression `assert(x == y)` will be turned into
53 * `assert(x == y, _d_assert_fail!(typeof(x))("==", x, y))`.
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).
61 * A string such as "$a $comp $b".
63 template _d_assert_fail(A
...)
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
69 // Prevent InvalidMemoryOperationError when triggered from a finalizer
71 return "Assertion failed (rich formatting is disabled in finalizers)";
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
.. $]);
83 /// Combines the supplied arguments into one string `"valA token valB"`
84 private string
combine(const scope string
[] valA
, const scope string token
,
85 const scope string
[] valB
) pure nothrow @nogc @safe
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
;
91 // Empty arrays are printed as ()
92 if (valA
.length
== 0) totalLen
+= 2;
93 if (valB
.length
== 0) totalLen
+= 2;
95 foreach (v
; valA
) totalLen
+= v
.length
;
96 foreach (v
; valB
) totalLen
+= v
.length
;
98 // Include braces when printing tuples
99 const printBraces
= (valA
.length
+ valB
.length
) != 2;
100 if (printBraces
) totalLen
+= 4; // '(', ')' for both tuples
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
)
106 if (printBraces
) buffer
[n
++] = '(';
107 foreach (idx
, v
; vals
)
114 buffer
[n
.. n
+ v
.length
] = v
;
117 if (printBraces
) buffer
[n
++] = ')';
121 formatTuple(buffer
, n
, valA
, printBraces
);
123 buffer
[n
.. n
+ token
.length
] = token
;
126 formatTuple(buffer
, n
, valB
, printBraces
);
127 return (() @trusted => cast(string
) buffer
)();
130 /// Yields the appropriate `printf` format token for a type `T`
131 private template getPrintfFormat(T
)
133 static if (is(T
== long))
135 enum getPrintfFormat
= "%lld";
137 else static if (is(T
== ulong))
139 enum getPrintfFormat
= "%llu";
141 else static if (__traits(isIntegral
, T
))
143 static if (__traits(isUnsigned
, T
))
145 enum getPrintfFormat
= "%u";
149 enum getPrintfFormat
= "%d";
154 static assert(0, "Unknown format");
159 * Generates a textual representation of `v` without relying on Phobos.
160 * The value is formatted as follows:
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)`
168 * Note that unions are rejected because this method cannot determine which
169 * member is valid when calling this method.
172 * v = the value to print
174 * Returns: a string respresenting `v` or `V.stringof` if `V` is not supported
176 private string
miniFormat(V
)(const scope ref V v
)
178 import core
.internal
.traits
: isAggregateType
;
180 /// `shared` values are formatted as their base type
181 static if (is(V
== shared T
, T
))
183 import core
.atomic
: atomicLoad
;
185 // Use atomics to avoid race conditions whenever possible
186 static if (__traits(compiles
, atomicLoad(v
)))
190 T tmp
= cast(T
) atomicLoad(v
);
191 return miniFormat(tmp
);
195 // Fall back to a simple cast - we're violating the type system anyways
196 return miniFormat(*cast(const T
*) &v
);
198 // Format enum members using their name
199 else static if (is(V BaseType
== enum))
201 // Always generate repeated if's instead of switch to skip the detection
202 // of non-integral enums. This method doesn't need to be fast.
203 static foreach (mem
; __traits(allMembers
, V
))
205 if (v
== __traits(getMember
, V
, mem
))
209 // Format invalid enum values as their base type
210 enum cast_
= "cast(" ~ V
.stringof
~ ")";
211 const val
= miniFormat(__ctfe ?
cast(const BaseType
) v
: *cast(const BaseType
*) &v
);
212 return combine([ cast_
], "", [ val
]);
214 else static if (is(V
== bool))
216 return v ?
"true" : "false";
218 // Detect vectors which match isIntegral / isFloating
219 else static if (is(V
== __vector(ET
[N
]), ET
, size_t N
))
227 msg
~= miniFormat(v
[i
]);
232 else static if (__traits(isIntegral
, V
))
234 static if (is(V
== char))
236 // Avoid invalid code points
238 return ['\'', v
, '\''];
241 return "cast(char) " ~ miniFormat(tmp
);
243 else static if (is(V
== wchar) ||
is(V
== dchar))
245 import core
.internal
.utf
: isValidDchar
, toUTF8
;
247 // Avoid invalid code points
249 return toUTF8(['\'', v
, '\'']);
252 return "cast(" ~ V
.stringof
~ ") " ~ miniFormat(tmp
);
256 import core
.internal
.string
;
257 static if (__traits(isUnsigned
, V
))
258 const val
= unsignedToTempString(v
);
260 const val
= signedToTempString(v
);
261 return val
.get().idup();
264 else static if (__traits(isFloating
, V
))
266 import core
.stdc
.stdio
: sprintf
;
267 import core
.stdc
.config
: LD
= c_long_double
;
269 // No suitable replacement for sprintf in druntime ATM
271 return '<' ~ V
.stringof
~ " not supported>";
273 // Workaround for https://issues.dlang.org/show_bug.cgi?id=20759
274 static if (is(LD
== real))
275 enum realFmt
= "%Lg";
281 static if (is(V
== float) ||
is(V
== double))
282 len
= sprintf(&val
[0], "%g", v
);
283 else static if (is(V
== real))
284 len
= sprintf(&val
[0], realFmt
, cast(LD
) v
);
285 else static if (is(V
== cfloat) ||
is(V
== cdouble))
286 len
= sprintf(&val
[0], "%g + %gi", v
.re
, v
.im
);
287 else static if (is(V
== creal))
288 len
= sprintf(&val
[0], realFmt
~ " + " ~ realFmt
~ 'i', cast(LD
) v
.re
, cast(LD
) v
.im
);
289 else static if (is(V
== ifloat) ||
is(V
== idouble))
290 len
= sprintf(&val
[0], "%gi", v
);
293 static assert(is(V
== ireal));
294 static if (is(LD
== real))
298 len
= sprintf(&val
[0], realFmt
~ 'i', cast(R
) v
);
300 return val
.idup
[0 .. len
];
302 // special-handling for void-arrays
303 else static if (is(V
== typeof(null)))
307 else static if (is(V
== U
*, U
))
309 // Format as ulong and prepend a 0x for pointers
310 import core
.internal
.string
;
311 return cast(immutable) ("0x" ~ unsignedToTempString
!16(cast(ulong) v
));
313 // toString() isn't always const, e.g. classes inheriting from Object
314 else static if (__traits(compiles
, { string s
= V
.init
.toString(); }))
316 // Object references / struct pointers may be null
317 static if (is(V
== class) ||
is(V
== interface))
325 // Prefer const overload of toString
326 static if (__traits(compiles
, { string s
= v
.toString(); }))
329 return (cast() v
).toString();
333 return `<toString() failed: "` ~ e
.msg
~ `", called on ` ~ formatMembers(v
) ~`>`;
336 // Static arrays or slices (but not aggregates with `alias this`)
337 else static if (is(V
: U
[], U
) && !isAggregateType
!V
)
339 import core
.internal
.traits
: Unqual
;
342 // special-handling for void-arrays
343 static if (is(E
== void))
346 return "<void[] not supported>";
348 const bytes
= cast(byte[]) v
;
349 return miniFormat(bytes
);
351 // anything string-like
352 else static if (is(E
== char) ||
is(E
== dchar) ||
is(E
== wchar))
354 const s
= `"` ~ v
~ `"`;
356 // v could be a char[], dchar[] or wchar[]
357 static if (is(typeof(s
) : const char[]))
358 return cast(immutable) s
;
361 import core
.internal
.utf
: toUTF8
;
368 foreach (i
, ref el
; v
)
373 // don't fully print big arrays
379 msg
~= miniFormat(el
);
385 else static if (is(V
: Val
[K
], K
, Val
))
389 foreach (ref k
, ref val
; v
)
393 // don't fully print big AAs
399 msg
~= miniFormat(k
) ~ ": " ~ miniFormat(val
);
404 else static if (is(V
== struct))
406 return formatMembers(v
);
408 // Extern C++ classes don't have a toString by default
409 else static if (is(V
== class) ||
is(V
== interface))
414 // Extern classes might be opaque
415 static if (is(typeof(v
.tupleof
)))
416 return formatMembers(v
);
418 return '<' ~ V
.stringof
~ '>';
426 /// Formats `v`'s members as `V(<member 1>, <member 2>, ...)`
427 private string
formatMembers(V
)(const scope ref V v
)
429 enum ctxPtr
= __traits(isNested
, V
);
430 enum isOverlapped
= calcFieldOverlap([ v
.tupleof
.offsetof
]);
432 string msg
= V
.stringof
~ "(";
433 foreach (i
, ref field
; v
.tupleof
)
438 static if (isOverlapped
[i
])
440 msg
~= "<overlapped field>";
444 // Mark context pointer
445 static if (ctxPtr
&& i
== v
.tupleof
.length
- 1)
446 msg
~= "<context>: ";
448 msg
~= miniFormat(field
);
456 * Calculates whether fields are overlapped based on the passed offsets.
459 * offsets = offsets of all fields matching the order of `.tupleof`
461 * Returns: an array such that arr[n] = true indicates that the n'th field
462 * overlaps with an adjacent field
464 private bool[] calcFieldOverlap(const scope size_t
[] offsets
)
466 bool[] overlaps
= new bool[](offsets
.length
);
468 foreach (const idx
; 1 .. overlaps
.length
)
470 if (offsets
[idx
- 1] == offsets
[idx
])
471 overlaps
[idx
- 1] = overlaps
[idx
] = true;
477 /// Negates a comparison token, e.g. `==` is mapped to `!=`
478 private string
invertCompToken(scope string comp
) pure nothrow @nogc @safe
503 assert(0, combine(["Invalid comparison operator '"], comp
, ["'"]));
507 /// Casts the function pointer to include `@safe`, `@nogc`, ...
508 private auto assumeFakeAttributes(T
)(T t
) @trusted
510 import core
.internal
.traits
: Parameters
, ReturnType
;
511 alias RT
= ReturnType
!T
;
512 alias P
= Parameters
!T
;
513 alias type
= RT
function(P
) nothrow @nogc @safe pure;
517 /// Wrapper for `miniFormat` which assumes that the implementation is `@safe`, `@nogc`, ...
518 /// s.t. it does not violate the constraints of the function containing the `assert`.
519 private string
miniFormatFakeAttributes(T
)(const scope ref T t
)
521 alias miniT
= miniFormat
!T
;
522 return assumeFakeAttributes(&miniT
)(t
);
525 /// Allocates an array of `t` bytes while pretending to be `@safe`, `@nogc`, ...
526 private auto pureAlloc(size_t t
)
528 static auto alloc(size_t len
)
530 return new ubyte[len
];
532 return assumeFakeAttributes(&alloc
)(t
);
535 /// Wrapper for GC.inFinalizer that fakes purity
536 private bool inFinalizer()() pure nothrow @nogc @safe
538 // CTFE doesn't trigger InvalidMemoryErrors
539 import core
.memory
: GC
;
540 return !__ctfe
&& assumeFakeAttributes(&GC
.inFinalizer
)();
543 // https://issues.dlang.org/show_bug.cgi?id=21544
546 // Normal enum values
549 assert(miniFormat(e
) == "A");
551 assert(miniFormat(e
) == "BCDE");
553 // Invalid enum value is printed as their implicit base type (int)
555 assert(miniFormat(e
) == "cast(E) 3");
557 // Non-integral enums work as well
564 enum E2
: S
{ a2
= S(1, "Hello") }
566 assert(miniFormat(es
) == `a2`);
568 // Even invalid values
569 es
= cast(E2
) S(2, "World");
570 assert(miniFormat(es
) == `cast(E2) S(2, "World")`);
576 static if (is(__vector(float[4])))
578 __vector(float[4]) f
= [-1.5f, 0.5f, 1.0f, 0.125f];
579 assert(miniFormat(f
) == "[-1.5, 0.5, 1, 0.125]");
582 static if (is(__vector(int[4])))
584 __vector(int[4]) i
= [-1, 0, 1, 3];
585 assert(miniFormat(i
) == "[-1, 0, 1, 3]");