1 /// When things go wrong....
2 module mecca.lib.exception;
3 
4 // Licensed under the Boost license. Full copyright information in the AUTHORS file
5 
6 public import core.exception: AssertError, RangeError;
7 public import std.exception: ErrnoException;
8 import std.algorithm : min, copy;
9 import std.traits;
10 import core.exception: assertHandler;
11 import core.runtime: Runtime, defaultTraceHandler;
12 
13 import mecca.log;
14 import mecca.lib.reflection: as;
15 import mecca.lib..string : nogcFormat, nogcRtFormat;
16 
17 // Disable tracing instrumentation for the whole file
18 @("notrace") void traceDisableCompileTimeInstrumentation();
19 
20 /// Returns `true` if an assert was thrown
21 ///
22 /// Will not be set to `true` under unittest builds, where an `AssertError` is thrown instead of fairly immediately
23 /// quitting
24 @property bool assertRaised() nothrow @safe @nogc {
25     return _assertInProgress;
26 }
27 
28 private bool _assertInProgress;
29 
30 // The default TraceInfo provided by DMD is not `@nogc`
31 private extern(C) nothrow @nogc {
32     int backtrace(void** buffer, int size);
33 
34     static if (__VERSION__ < 2077) {
35         pragma(mangle, "_D4core7runtime19defaultTraceHandlerFPvZ16DefaultTraceInfo6__ctorMFZC4core7runtime19defaultTraceHandlerFPvZ16DefaultTraceInfo")
36             void defaultTraceInfoCtor(Object);
37     } else static if (__VERSION__ < 2088) {
38         pragma(mangle, "_D4core7runtime19defaultTraceHandlerFPvZ16DefaultTraceInfo6__ctorMFZCQCpQCnQCiFQBqZQBr")
39             void defaultTraceInfoCtor(Object);
40     } else {
41         pragma(mangle, "_D4core7runtime16DefaultTraceInfo6__ctorMFZCQBqQBoQBj")
42             void defaultTraceInfoCtor(Object);
43     }
44 }
45 private __gshared static TypeInfo_Class defaultTraceTypeInfo;
46 
47 shared static this() {
48     defaultTraceTypeInfo = typeid(cast(Object)defaultTraceHandler(null));
49     assert(defaultTraceTypeInfo.name == "core.runtime.defaultTraceHandler.DefaultTraceInfo", defaultTraceTypeInfo.name);
50     assert(defaultTraceTypeInfo.initializer.length <= ExcBuf.MAX_TRACEBACK_SIZE);
51     assert(DefaultTraceInfoABI.sizeof <= defaultTraceTypeInfo.initializer.length);
52     version (unittest) {} else {
53         assertHandler = &assertHandlerImpl;
54     }
55 }
56 
57 extern(C) private void ALREADY_EXTRACTING_STACK() {
58     assert (false, "you're not supposed to call this function");
59 }
60 
61 /**
62  * Extract the stack backtrace.
63  *
64  * The pointers returned from the function are one less than the actual number. This is so that a symbol lookup will report the
65  * correct function even when the call to _d_throw_exception was the last thing in it.
66  *
67  * Params:
68  * callstack = range to receive the call stack pointers
69  * skip = number of near frames to skip.
70  *
71  * Retruns:
72  * Range where the actual pointers reside.
73  */
74 void*[] extractStack(void*[] callstack, size_t skip = 0) nothrow @trusted @nogc {
75     /* thread local */ static bool alreadyExtractingStack = false;
76     if (alreadyExtractingStack) {
77         // stack extraction is apparently non re-entrant, and because we want signal handlers
78         // to be able to use this function, we just return a mock stack in this case.
79         callstack[0] = &ALREADY_EXTRACTING_STACK;
80         callstack[1] = &ALREADY_EXTRACTING_STACK;
81         callstack[2] = &ALREADY_EXTRACTING_STACK;
82         callstack[3] = null;
83         return callstack[0 .. 4];
84     }
85     alreadyExtractingStack = true;
86     scope(exit) alreadyExtractingStack = false;
87 
88     auto numFrames = backtrace(callstack.ptr, cast(int)callstack.length);
89     auto res = callstack[skip .. numFrames];
90 
91     // Adjust the locations by one byte so they point inside the function (as
92     // required by backtrace_symbols) even if the call to _d_throw_exception
93     // was the very last instruction in the function.
94     foreach (ref c; res) {
95         c -= 1;
96     }
97     return res;
98 }
99 
100 struct DefaultTraceInfoABI {
101     import mecca.lib.reflection: isVersion;
102 
103     void*    _vtbl;
104     void*    _monitor;
105     void*    _interface;  // introduced in DMD 2.071
106     // make sure the ABI matches
107     static assert ({static interface I {} static class C: I {} return __traits(classInstanceSize, C);}() == (void*[3]).sizeof);
108 
109     int      numframes;
110     void*[0] callstack;
111 
112     static DefaultTraceInfoABI* extract(Throwable.TraceInfo traceInfo) nothrow @trusted @nogc {
113         auto obj = cast(Object)traceInfo;
114         assert (typeid(obj).name == "core.runtime.defaultTraceHandler.DefaultTraceInfo", typeid(obj).name);
115         return cast(DefaultTraceInfoABI*)(cast(void*)obj);
116     }
117     static DefaultTraceInfoABI* extract(Throwable ex) nothrow @safe @nogc {
118         return extract(ex.info);
119     }
120     @property void*[] frames() return nothrow @trusted @nogc {
121         return callstack.ptr[0 .. numframes];
122     }
123 }
124 
125 /// Static buffer for storing no GC exceptions
126 struct ExcBuf {
127     enum MAX_EXCEPTION_INSTANCE_SIZE = 256;
128     enum MAX_EXCEPTION_MESSAGE_SIZE = 256;
129     enum MAX_TRACEBACK_SIZE = 1064;
130 
131     ubyte[MAX_EXCEPTION_INSTANCE_SIZE] ex;
132     ubyte[MAX_TRACEBACK_SIZE] ti;
133     char[MAX_EXCEPTION_MESSAGE_SIZE] msgBuf;
134 
135     /// Get the Throwable stored in the buffer
136     Throwable get() return nothrow @trusted @nogc {
137         if (*(cast(void**)ex.ptr) is null) {
138             return null;
139         }
140         return cast(Throwable)ex.ptr;
141     }
142 
143     /// Set the exception that buffer is to hold.
144     Throwable set(Throwable t, bool setTraceback = false) nothrow @trusted @nogc {
145         static assert (this.ex.offsetof == 0);
146         if (t is null) {
147             *(cast(void**)ex.ptr) = null;
148             return null;
149         }
150 
151         if (t is get()) {
152             // don't assign to yourself to yourself
153             if (setTraceback) {
154                 this.setTraceback(t);
155             }
156             return t;
157         }
158 
159         auto len = typeid(t).initializer.length;
160         ex[0 .. len] = (cast(ubyte*)t)[0 .. len];
161         auto tobj = cast(Throwable)ex.ptr;
162 
163         if (setTraceback) {
164             this.setTraceback(t);
165         }
166         else {
167             if (t.info is null) {
168                 tobj.info = null;
169                 *(cast(void**)ti.ptr) = null;
170             }
171             else {
172                 auto tinfo = cast(Object)t.info;
173                 assert (tinfo !is null, "casting TraceInfo to Object failed");
174                 len = typeid(tinfo).m_init.length;
175                 ti[0 .. len] = (cast(ubyte*)tinfo)[0 .. len];
176                 tobj.info = cast(Throwable.TraceInfo)(cast(Object)ti.ptr);
177             }
178         }
179 
180         setMsg(t.msg);
181         return tobj;
182     }
183 
184     /**
185      * set a Throwable's backtrace to the current point.
186      *
187      * Params:
188      * tobj = the Throwable which backtrace to set. If null, set the buffer's Throwable (which must not be null).
189      */
190     void setTraceback(Throwable tobj) nothrow @trusted @nogc {
191         if (tobj is null) {
192             tobj = get();
193             assert (tobj !is null, "setTraceback of unset exception");
194         }
195         ti[0 .. DefaultTraceInfoABI.sizeof] = cast(ubyte[])(defaultTraceTypeInfo.initializer[0 .. DefaultTraceInfoABI.sizeof]);
196         auto tinfo = cast(Object)ti.ptr;
197         defaultTraceInfoCtor(tinfo);
198         tobj.info = cast(Throwable.TraceInfo)tinfo;
199     }
200 
201     /**
202      * Construct a throwable in place
203      *
204      * Params:
205      * file = the reported source file of the exception
206      * line = the reported source file line of the exception
207      * setTraceback = whether to set the stack trace
208      * args = arguments to pass to the exception's constructor
209      */
210     T construct(T:Throwable, A...)(string file, size_t line, bool setTraceback, auto ref A args) nothrow @trusted @nogc
211     {
212         T t = constructHelper!T(file, line, setTraceback, args);
213         setMsg(t.msg, t);
214         return t;
215     }
216 
217     /**
218      * Construct a Throwable, formatting the message
219      *
220      * Construct a `Throwable` that has a constructor that accepts a single string argument (such as `Exception`). Allows
221      * formatting arguments into the string in a non GC way.
222      *
223      * Second form of the function receives the string as a template argument, and verifies that the arguments match the
224      * format string.
225      */
226     T constructFmt(T: Throwable = Exception, A...)(string file, size_t line, string fmt, auto ref A args) @trusted {
227         auto tmpMsg = nogcRtFormat(msgBuf[], fmt, args);
228         return constructHelper!T(file, line, true, tmpMsg);
229     }
230 
231     unittest {
232         ExcBuf ex;
233         ex.constructFmt!Exception("file.d", 31337, "%s was a %s %s", "pappa", "rolling", "stoner");
234 
235         assert( ex.get().msg=="pappa was a rolling stoner" );
236     }
237 
238     /// ditto
239     T constructFmt(string fmt, T: Throwable = Exception, A...)(string file, size_t line, auto ref A args) @trusted {
240         auto tmpMsg = nogcFormat!fmt(msgBuf[], args);
241         return constructHelper!T(file, line, true, tmpMsg);
242     }
243 
244     unittest {
245         ExcBuf ex;
246         ex.constructFmt!("I'm %s in %s", Exception)("file.d", 31337, "an Englishman", "New York");
247 
248         assert( ex.get().msg=="I'm an Englishman in New York" );
249 
250         static assert( !__traits(compiles,
251                 ex.constructFmt!("No arguments, please", Exception)("file.d", 31337, "An argument")) );
252     }
253 
254     void setMsg(const(char)[] msg2, Throwable tobj = null) nothrow @nogc {
255         if (tobj is null) {
256             tobj = get();
257             assert (tobj);
258         }
259         if (msg2 is null || msg2.length == 0) {
260             tobj.msg = null;
261         }
262         else if (msg2.length > msgBuf.length) {
263             msgBuf[] = msg2[0 .. msgBuf.length];
264             tobj.msg = cast(string)msgBuf[];
265         }
266         else {
267             // msgBuf[0 .. msg2.length] = msg2[];
268             // The buffer we copy from and to are, occasionally, the same buffer. The above line is illegal in that case
269             copy(msg2, msgBuf[0..msg2.length]);
270             tobj.msg = cast(string)msgBuf[0 .. msg2.length];
271         }
272     }
273 
274     static bool isGCException(Throwable ex) {
275         import core.memory: GC;
276         return GC.addrOf(cast(void*)ex) !is null;
277     }
278 
279     static Throwable toGC(Throwable ex, bool forceCopy=false) {
280         if (!forceCopy && isGCException(ex)) {
281             // already GC-allocated
282             return ex;
283         }
284         auto buf = new ExcBuf;
285         return buf.set(ex);
286     }
287 
288 private:
289     T constructHelper(T:Throwable, A...)(
290             string file, size_t line, bool setTraceback, auto ref A args) nothrow @trusted @nogc
291     {
292         static assert (__traits(classInstanceSize, T) <= ExcBuf.MAX_EXCEPTION_INSTANCE_SIZE);
293 
294         // create the exception
295         ex[0 .. __traits(classInstanceSize, T)] = cast(ubyte[])typeid(T).initializer[];
296         auto t = cast(T)ex.ptr;
297         as!"nothrow @nogc"({t.__ctor(args);});
298         t.file = file;
299         t.line = line;
300         t.info = null;
301 
302         if (setTraceback) {
303             this.setTraceback(t);
304         }
305 
306         return t;
307     }
308 }
309 
310 /* thread local*/ static ExcBuf _tlsExcBuf;
311 /* thread local*/ static ExcBuf* _currExcBuf;
312 /* thread local*/ static this() {_currExcBuf = &_tlsExcBuf;}
313 
314 void switchCurrExcBuf(ExcBuf* newCurrentExcBuf) nothrow @safe @nogc {
315     if (newCurrentExcBuf !is null)
316         _currExcBuf = newCurrentExcBuf;
317     else
318         _currExcBuf = &_tlsExcBuf;
319 }
320 
321 T mkEx(T: Throwable, string file = __FILE_FULL_PATH__, size_t line = __LINE__, A...)(auto ref A args) @safe @nogc {
322     version(LDC)
323             // Must inline because of __FILE_FULL_PATH__ as template parameter. https://github.com/ldc-developers/ldc/issues/1703
324             pragma(inline, true);
325 
326     return mkExLine!T(file, line, args);
327 }
328 
329 T mkExLine(T: Throwable, A...)(string file, size_t line, auto ref A args) @trusted @nogc {
330     static assert(!isNested!T, "Cannot use mkEx to create exception of type " ~ T.stringof ~ " that needs a context pointer");
331     return _currExcBuf.construct!T(file, line, true, args);
332 }
333 
334 T mkExFmt(T: Throwable, string file = __FILE_FULL_PATH__, size_t line = __LINE__, A...)(string fmt, auto ref A args) @safe @nogc
335 {
336     version(LDC)
337             // Must inline because of __FILE_FULL_PATH__ as template parameter. https://github.com/ldc-developers/ldc/issues/1703
338             pragma(inline, true);
339 
340     return _currExcBuf.constructFmt!T(file, line, fmt, args);
341 }
342 
343 T mkExFmt(string fmt, T: Throwable = Exception, string file = __FILE_FULL_PATH__, size_t line = __LINE__, A...)(auto ref A args)
344     @safe @nogc
345 {
346     version(LDC)
347             // Must inline because of __FILE_FULL_PATH__ as template parameter. https://github.com/ldc-developers/ldc/issues/1703
348             pragma(inline, true);
349 
350     return _currExcBuf.constructFmt!(fmt, T)(file, line, args);
351 }
352 
353 Throwable setEx(Throwable ex, bool setTraceback = false) nothrow @safe @nogc {
354     return _currExcBuf.set(ex, setTraceback);
355 }
356 
357 class RangeErrorWithReason : Error {
358     mixin ExceptionBody;
359 }
360 
361 RangeErrorWithReason rangeError(K, string file=__FILE_FULL_PATH__, string mod=__MODULE__, size_t line=__LINE__)
362     (K key, string msg = "Index/key not found")
363 {
364     version(LDC)
365             // Must inline because of __FILE_FULL_PATH__ as template parameter. https://github.com/ldc-developers/ldc/issues/1703
366             pragma(inline, true);
367 
368     import std.format : format;
369     static if ( __traits(compiles, format("%s", key))) {
370         return mkExFmt!("%s: %s", RangeErrorWithReason, file, line)(msg, key);
371     }
372     else {
373         return mkExFmt!("%s: %s", RangeErrorWithReason, file, line)(msg, K.stringof);
374     }
375 }
376 
377 void enforceFmt(T: Throwable = Exception, string file = __FILE_FULL_PATH__, size_t line = __LINE__, A...)(
378         bool cond, string fmt, auto ref A args) @safe @nogc
379 {
380     version(LDC)
381             // Must inline because of __FILE_FULL_PATH__ as template parameter. https://github.com/ldc-developers/ldc/issues/1703
382             pragma(inline, true);
383 
384     if (!cond) {
385         throw mkExFmt!(T, file, line)(fmt, args);
386     }
387 }
388 
389 
390 mixin template ExceptionBody(string msg) {
391     this(string file = __FILE_FULL_PATH__, size_t line = __LINE__, Throwable next = null) @safe pure nothrow @nogc {
392         super(msg, file, line, next);
393     }
394 }
395 
396 mixin template ExceptionBody() {
397     this(string msg, string file = __FILE_FULL_PATH__, size_t line = __LINE__, Throwable next = null) @safe pure nothrow @nogc {
398         super(msg, file, line, next);
399     }
400     /+import mecca.lib.exception: ExcBuf;
401     __gshared static ExcBuf* _singletonExBuf;
402 
403     shared static this() {
404         _singletonExBuf = new ExcBuf;
405     }
406     @("notrace") static typeof(this) singleton(string file = __FILE_FULL_PATH__, size_t line = __LINE__) {
407         import mecca.tracing.api: LOG_TRACEBACK;
408         auto ex = _singletonExBuf.construct!(typeof(this))(file, line, true, (staticMsg.length == 0 ? typeof(this).stringof : staticMsg));
409         LOG_TRACEBACK(ex);
410         return ex;
411     }+/
412 }
413 
414 unittest {
415     import std.stdio;
416     static class MyException: Exception {mixin ExceptionBody;}
417     static class YourException: Exception {mixin ExceptionBody;}
418 
419     bool thrown1 = false;
420     try {
421         throw mkEx!MyException("hello world");
422     }
423     catch (MyException ex) {
424         assert (ex.msg == "hello world");
425         thrown1 = true;
426     }
427     assert (thrown1);
428 
429     auto ex2 = new YourException("foo bar");
430     bool thrown2 = false;
431     try {
432         throw setEx(ex2);
433     }
434     catch (YourException ex) {
435         assert (ex !is ex2);
436         assert(ex.msg == "foo bar");
437         thrown2 = true;
438     }
439     assert (thrown2);
440 }
441 
442 private @notrace void assertHandlerImpl(string file, size_t line, string msg) nothrow @nogc {
443     pragma(inline, true);
444     DIE(msg, file, line);
445 }
446 
447 void function(string msg, string file, size_t line) blowUpHandler;
448 
449 @notrace void DIE(string msg, string file = __FILE_FULL_PATH__, size_t line = __LINE__, bool doAbort=false) nothrow @nogc {
450     import core.sys.posix.unistd: write, _exit;
451     import core.stdc.stdlib: abort;
452     import core.atomic: cas;
453 
454     // block threads racing into this function
455     shared static bool recLock = false;
456     while (!cas(&recLock, false, true)) {}
457 
458     __gshared static ExcBuf excBuf;
459     as!"nothrow @nogc"({
460         if( loggingInitialized ) {
461             META!"Assertion failure(%s:%s) %s"(file, line, msg);
462             flushLog();
463         }
464         auto ex = excBuf.construct!AssertError(file, line, true, msg);
465         version(unittest) {
466             recLock = false;
467             throw ex;
468         } else {
469             _assertInProgress = true;
470             ex.toString((text){write(2, text.ptr, text.length);});
471             if (doAbort) {
472                 abort();
473             }
474             else {
475                 _exit(1);
476             }
477         }
478     });
479     assert(false);
480 }
481 
482 unittest {
483     assertThrows!AssertError( DIE("Test UT DIE behavior") );
484 }
485 
486 void ABORT(string msg, string file = __FILE_FULL_PATH__, size_t line = __LINE__) nothrow @nogc {
487     DIE(msg, file, line, true);
488 }
489 
490 @notrace void ASSERT
491     (string fmt, string file = __FILE_FULL_PATH__, string mod = __MODULE__, size_t line = __LINE__, T...)
492     (bool cond, scope lazy T args)
493     pure nothrow @trusted @nogc
494 {
495     version(LDC)
496             // Must inline because of __FILE_FULL_PATH__ as template parameter. https://github.com/ldc-developers/ldc/issues/1703
497             pragma(inline, true);
498 
499     if (cond) {
500         return;
501     }
502 
503     scope f = () {
504         META!("ASSERT: " ~ fmt, file, mod, line)(args);
505         static if( !LogToConsole ) {
506             // Also log to stderr, as the logger doesn't do that for us.
507             import std.stdio: stderr;
508             stderr.writefln( "Assertion failure at %s:%s: " ~ fmt, file, line, args );
509         }
510     };
511     as!"@nogc pure nothrow"(f);
512     version(unittest ){
513         as!"@nogc pure nothrow"({
514             import std..string: format;
515             throw new AssertError( format("Assertion failure: " ~ fmt, args), file, line);
516         });
517     }
518     else {
519         as!"pure"({
520             _assertInProgress = true;
521             dumpStackTrace();
522             DIE("Assertion failed", file, line);
523         });
524     }
525 }
526 
527 void enforceNGC(Ex : Throwable = Exception, string file = __FILE_FULL_PATH__, size_t line = __LINE__)
528     (bool value, scope lazy string msg = null) @trusted @nogc
529 {
530     version(LDC)
531             // Must inline because of __FILE_FULL_PATH__ as template parameter. https://github.com/ldc-developers/ldc/issues/1703
532             pragma(inline, true);
533 
534     if( !value ) {
535         string evaluatedMsg;
536         as!"@nogc"({evaluatedMsg = msg;});
537         throw mkEx!Ex(evaluatedMsg, file, line);
538     }
539 }
540 
541 void errnoEnforceNGC(string file = __FILE_FULL_PATH__, size_t line = __LINE__)
542     (bool value, scope lazy string msg = null) @trusted @nogc
543 {
544     version(LDC)
545             // Must inline because of __FILE_FULL_PATH__ as template parameter. https://github.com/ldc-developers/ldc/issues/1703
546             pragma(inline, true);
547 
548     as!"@nogc"({ enforceNGC!(ErrnoException, file, line)(value, msg); });
549 }
550 
551 version(assert) {
552     alias DBG_ASSERT = ASSERT;
553 }
554 else {
555     void DBG_ASSERT(string fmt, string file = __FILE_FULL_PATH__, string mod = __MODULE__, size_t line = __LINE__, T...)
556             (scope lazy bool cond, scope lazy T args) @nogc
557     {
558         pragma(inline, true);
559     }
560 }
561 
562 unittest {
563     ASSERT!"oh no: %s"(true, "foobar");
564 }
565 
566 
567 /** Assert on a generic operation
568  *
569  * This is a generic assert based on an operation between two arguments. This function's advantage over merely asserting
570  * with the operation is that, in case of assert failure, both values compared are printed in addition to the assert
571  * message.
572  *
573  * If asserts are disabled, this function compiles away to nothing.
574  */
575 @notrace void assertOp(string op, L, R, string file = __FILE_FULL_PATH__, string mod = __MODULE__, size_t line = __LINE__)
576     (L lhs, R rhs, scope lazy string msg="") nothrow
577 {
578     version(assert) {
579         version(LDC)
580                 // Must inline because of __FILE_FULL_PATH__ as template parameter. https://github.com/ldc-developers/ldc/issues/1703
581                 pragma(inline, true);
582 
583         static assert(
584                 !is(Unqual!LHS == enum) || is(Unqual!LHS == Unqual!RHS),
585                 "comparing different enums is unsafe: " ~ LHS.stringof ~ " != " ~ RHS.stringof);
586 
587         import std.meta: staticIndexOf;
588         enum idx = staticIndexOf!(op, "==", "!=", ">", "<", ">=", "<=", "in", "!in", "is", "!is");
589         static assert (idx >= 0, "assertOp called with operation \"" ~ op ~ "\" which is not supported");
590         enum inverseOp = ["!=", "==", "<=", ">=", "<", ">", "!in", "in", "!is", "is"][idx];
591 
592         auto lhsVal = lhs;
593         auto rhsVal = rhs;
594 
595         if( mixin("lhsVal " ~ op ~ " rhsVal") )
596             return;
597 
598         void safefify() pure @nogc @trusted {
599             as!"@nogc pure nothrow"({
600                 import std.format;
601                 DIE(format("Assert: %s %s %s %s", lhs, inverseOp, rhs, msg), file, line);
602             });
603         }
604 
605         safefify();
606     }
607 }
608 
609 /// Assert that two values are equal.
610 @notrace void assertEQ(L, R, string file = __FILE_FULL_PATH__, string mod = __MODULE__, size_t line = __LINE__)(
611         L lhs, R rhs, scope lazy string msg="") nothrow @trusted
612 {
613     assertOp!("==", L, R, file, mod, line)(lhs, rhs, as!"@nogc nothrow"( {return msg;} ));
614 }
615 /// Assert that two values are not equal.
616 @notrace void assertNE(L, R, string file = __FILE_FULL_PATH__, string mod = __MODULE__, size_t line = __LINE__)(
617         L lhs, R rhs, scope lazy string msg="") nothrow @trusted
618 {
619     assertOp!("!=", L, R, file, mod, line)(lhs, rhs, as!"@nogc nothrow"( {return msg;} ));
620 }
621 /// Assert that `lhs` is greater than `rhs`.
622 @notrace void assertGT(L, R, string file = __FILE_FULL_PATH__, string mod = __MODULE__, size_t line = __LINE__)(
623         L lhs, R rhs, scope lazy string msg="") nothrow @trusted
624 {
625     assertOp!(">", L, R, file, mod, line)(lhs, rhs, as!"@nogc nothrow"( {return msg;} ));
626 }
627 /// Assert that `lhs` is greater or equals to `rhs`.
628 @notrace void assertGE(L, R, string file = __FILE_FULL_PATH__, string mod = __MODULE__, size_t line = __LINE__)(
629         L lhs, R rhs, scope lazy string msg="") nothrow @trusted
630 {
631     assertOp!(">=", L, R, file, mod, line)(lhs, rhs, as!"@nogc nothrow"( {return msg;} ));
632 }
633 /// Assert that `lhs` is lesser than `rhs`.
634 @notrace void assertLT(L, R, string file = __FILE_FULL_PATH__, string mod = __MODULE__, size_t line = __LINE__)(
635         L lhs, R rhs, scope lazy string msg="") nothrow @trusted
636 {
637     assertOp!("<", L, R, file, mod, line)(lhs, rhs, as!"@nogc nothrow"( {return msg;} ));
638 }
639 /// Assert that `lhs` is lesser or equal to `rhs`.
640 @notrace void assertLE(L, R, string file = __FILE_FULL_PATH__, string mod = __MODULE__, size_t line = __LINE__)(
641         L lhs, R rhs, scope lazy string msg="") nothrow @trusted
642 {
643     assertOp!("<=", L, R, file, mod, line)(lhs, rhs, as!"@nogc nothrow"( {return msg;} ));
644 }
645 
646 version(unittest) {
647     void assertThrows(T = Throwable, E, string file = __FILE_FULL_PATH__, string mod = __MODULE__, size_t line = __LINE__)
648             (scope lazy E expr)
649     {
650         try {
651             expr();
652         }
653         catch (Throwable ex) {
654             ASSERT!("Threw %s instead of %s", file, mod, line)(cast(T)ex !is null, typeid(ex).name, T.stringof);
655             return;
656         }
657         ASSERT!("Did not throw", file, mod, line)(false);
658     }
659 
660     unittest {
661         static bool thrower(T : Throwable)(string msg) {
662             throw new T(msg);
663         }
664 
665         try {
666             assertThrows!AssertError( 12 );
667             assert(false, "assertThrows did not detect code did not throw");
668         } catch(AssertError ex) {
669         }
670 
671         try {
672             assertThrows!AssertError( thrower!ErrnoException("Nothing wrong") );
673             assert(false, "assertThrows did not detect code threw the wrong exception");
674         } catch(AssertError ex) {
675         }
676     }
677 }
678 
679 unittest {
680     assertEQ(7, 7);
681     assertNE(7, 17);
682     assertThrows!AssertError(assertEQ(7, 17));
683 }
684 
685 int errnoCall(alias F, string file=__FILE_FULL_PATH__, size_t line=__LINE__)(Parameters!F args) @nogc if (is(ReturnType!F == int))
686 {
687     version(LDC)
688             // Must inline because of __FILE_FULL_PATH__ as template parameter. https://github.com/ldc-developers/ldc/issues/1703
689             pragma(inline, true);
690 
691     int res = F(args);
692     if (res < 0) {
693         import std.range: repeat;
694         import std..string;
695         enum fmt = __traits(identifier, F) ~ "(" ~ "%s".repeat(args.length).join(", ") ~ ")";
696         throw mkExFmt!(fmt, ErrnoException, file, line)(args);
697     }
698     return res;
699 }
700 
701 unittest {
702     import core.sys.posix.unistd: dup, close;
703     auto newFd = errnoCall!dup(1);
704     assert (newFd >= 0);
705     errnoCall!close(newFd);
706     // double close will throw
707     assertThrows!ErrnoException(errnoCall!close(newFd));
708 }