1 /**
2    Implements a mixin template that allows throwing Errors from `@nogc` code.
3  */
4 
5 module alid.errornogc;
6 
7 /**
8     Mixes in the definition of a subclass of `Error` as well as a convenience
9     function (e.g. `myError()`) to throw the single thread-local instance of
10     that class.
11 
12     The creation of the single instance is `@nogc` because the object that is
13     thrown is emplaced on a thread-local memory buffer (both the object and the
14     buffer it contains are allocated lazily). And throwing is `@nogc` because
15     the thrown object is the single thread-local instance. Each `NogcError`
16     object can carry arbitrary number of data of arbitrary types, which are
17     emplaced inside the single error object. The size of data storage is
18     specified with the `maxDataSize` template parameter.
19 
20     The following examples use the `NogcError!"foo"` type and its associated
21     `fooError()` function, which can be defined similarly to the following code:
22     ---
23     // Define NogcError_!"foo", which will be thrown by calling fooError():
24     mixin NogcError!"foo";
25     ---
26 
27     Params:
28 
29         tag = a differentiating type _tag to allow multiple NogcError types with
30               their associated single instances
31 
32         maxDataSize = the size of the buffer to hold additional data accompanying
33                       the thrown error object
34 
35     Bugs:
36 
37         The error data that is emplaced inside the error object are never
38         destroyed. This decision is supported by the realization that the
39         program is about to end due to the thrown NogcError.
40  */
41 mixin template NogcError(string tag, size_t maxDataSize = 1024)
42 {
43     private class NogcError_ : Error
44     {
45         string msg;                // Main error message
46         ubyte[maxDataSize] data_;  // Additional information associated with the error
47         size_t dataOffset;         // Determines where aligned data starts
48         size_t dataSize;           // Determines the size of data
49 
50         // The lambda that knows how to print the type-erased data
51         void function(void delegate(in char[]), const(ubyte)*) dataToStr;
52 
53         enum name = `NogcError!"` ~ tag ~ '"';
54 
55         this() @nogc nothrow pure @safe scope
56         {
57             super(name);
58         }
59 
60         // Where actual data is at after considering alignment offset
61         inout(ubyte)* dataStart_() inout @nogc nothrow pure @trusted scope
62         {
63             return data_.ptr + dataOffset;
64         }
65 
66         // Adapted from object.Throwable.toString
67         override
68         void toString(scope void delegate(in char[]) sink) const nothrow scope
69         {
70             try
71             {
72                 import std.conv : to;
73 
74                 sink(file); sink(":"); sink(line.to!string); sink(": ");
75                 sink(name); sink(": "); sink(msg);
76 
77                 if (dataSize)
78                 {
79                     sink("\n  Data: ");
80                     if (dataToStr)
81                     {
82                         dataToStr(sink, dataStart_);
83                     }
84                 }
85 
86                 if (info)
87                 {
88                     sink("\n----------------");
89                     foreach (t; info)
90                     {
91                         sink("\n"); sink(t);
92                     }
93                 }
94             }
95             catch (Throwable)
96             {
97                 // ignore more errors
98             }
99         }
100     }
101 
102     /*
103       Allow access to the per-thread NogcError!tag instance.
104 
105       The template constraint is to prevent conflicting mixed-in definitions of
106       unrelated NogcError instantiations.
107     */
108     private static ref theError(string t)() @nogc nothrow @trusted
109     if (t == tag)
110     {
111         static ubyte[__traits(classInstanceSize, NogcError_)] mem_;
112         static NogcError_ obj_;
113 
114         if (!obj_)
115         {
116             import core.lifetime : emplace;
117             obj_ = emplace!NogcError_(mem_[]);
118         }
119 
120         return obj_;
121     }
122 
123     private static string throwNogcError(Data...)(
124         in string msg, auto ref Data data, in string file, in int line)
125             @nogc nothrow pure @safe
126     {
127         static thrower(in string msg, Data data, in string file, in int line)
128             @nogc nothrow @trusted
129         {
130             import core.lifetime : emplace;
131             import std.algorithm : max;
132             import std.format : format;
133             import std.typecons : Tuple;
134 
135             alias TD = Tuple!Data;
136             enum size = (Data.length ? TD.sizeof : 0);
137             enum alignment = max(TD.alignof, (void*).alignof);
138 
139             // Although this should never happen, being safe before the
140             // subtraction below
141             static assert (alignment > 0);
142 
143             // We will consider alignment against the worst case run-time
144             // situation by assuming that the modulus operation would produce 1
145             // at run time (very unlikely).
146             enum maxDataOffset = alignment - 1;
147 
148             static assert((theError!tag.data_.length >= maxDataOffset) &&
149                           (size <= (theError!tag.data_.length - maxDataOffset)),
150                           format!("Also considering the %s-byte alignment of %s, it is" ~
151                                   " not possible to fit %s bytes into a %s-byte buffer.")(
152                                       alignment, Data.stringof, size, maxDataSize));
153 
154             theError!tag.msg = msg;
155             theError!tag.file = file;
156             theError!tag.line = line;
157 
158             // Ensure correct alignment
159             const extra = cast(ulong)theError!tag.data_.ptr % alignment;
160             theError!tag.dataOffset = alignment - extra;
161             theError!tag.dataSize = size;
162 
163             emplace(cast(TD*)(theError!tag.dataStart_), TD(data));
164 
165             // Save for later printing
166             theError!tag.dataToStr = (sink, ptr)
167                                      {
168                                          import std.conv : to;
169 
170                                          auto d = cast(TD*)(ptr);
171                                          static foreach (i; 0 .. TD.length)
172                                          {
173                                              static if (i != 0)
174                                              {
175                                                  sink(", ");
176                                              }
177                                              sink((*d)[i].to!string);
178                                          }
179                                      };
180 
181             // We can finally throw the single error object
182             throw theError!tag;
183         }
184 
185         // Adapted from std/regex/internal/ir.d
186         static assumePureFunction(T)(in T t) @nogc nothrow pure @trusted
187         {
188             import std.traits :
189                 FunctionAttribute, functionAttributes,
190                 functionLinkage, SetFunctionAttributes;
191 
192             enum attrs = functionAttributes!T | FunctionAttribute.pure_;
193             return cast(SetFunctionAttributes!(T, functionLinkage!T, attrs))t;
194         }
195 
196         assumePureFunction(&thrower)(msg, data, file, line);
197 
198         assert(false, "A NogcError should have been thrown.");
199     }
200 
201     /*
202         Inject the function for <tag>Error() calls like myError(), yourError(),
203         etc. Although the return type of the function is 'string' to satisfy
204         e.g. contracts, the function does not return because throwNogcError()
205         that it calls throws.
206     */
207     mixin (`string ` ~ tag ~ `Error(Data...)` ~
208            `(in string msg, in Data data,` ~
209            ` in string file = __FILE__, in int line = __LINE__)` ~
210            ` @nogc nothrow pure @safe` ~
211            `{ return throwNogcError(msg, data, file, line); }`);
212 
213     // This version is a workaround for some cases where 'file' and 'line' would
214     // become a part of 'data'.
215     mixin (`string ` ~ tag ~ `ErrorFileLine(Data...)` ~
216            `(in string file, in int line, in string msg, in Data data)` ~
217            ` @nogc nothrow pure @safe` ~
218            `{ return throwNogcError(msg, data, file, line); }`);
219 }
220 
221 ///
222 unittest
223 {
224     /*
225         Throwing from a pre-condition.
226 
227         In this case, the error is thrown while generating the string that the
228         failed pre-condition is expecting. Such a string will never arrive at
229         the pre-condition code.
230     */
231     void test_1(int i)
232     in (i > 0, fooError("The value must be positive", i, 42))
233     {
234         // ...
235     }
236     /*
237         The .msg property of the error contains both the error string and the
238         data that is included in the error.
239     */
240     assertErrorStringContains(() => test_1(-1), [ "The value must be positive",
241                                                   "-1, 42" ]);
242 
243     // Throwing from the body of a function
244     void test_2(int i)
245     {
246         string otherData = "hello world";
247         fooError("Something went wrong", otherData);
248     }
249     assertErrorStringContains(() => test_2(0), [ "Something went wrong",
250                                                  "hello world" ]);
251 
252     // Throwing without any data
253     void test_3()
254     {
255         fooError("Something is bad");
256     }
257     assertErrorStringContains(() => test_3(), [ "Something is bad" ]);
258 }
259 
260 version (unittest)
261 {
262     // Define NogcError!"foo", which will be thrown by calling fooError():
263     private mixin NogcError!"foo";
264 
265     // Assert that the expression throws an Error object and that its string
266     // representation contains all expected strings.
267     private void assertErrorStringContains(void delegate() expr, string[] expected)
268     {
269         bool thrown = false;
270 
271         try
272         {
273             expr();
274         }
275         catch (Error err)
276         {
277             thrown = true;
278 
279             import std.algorithm : any, canFind, splitter;
280             import std.conv : to;
281             import std.format : format;
282 
283             auto lines = err.to!string.splitter('\n');
284             foreach (exp; expected)
285             {
286                 assert(lines.any!(line => line.canFind(exp)),
287                        format!"Failed to find \"%s\" in the output: %-(\n  |%s%)"(
288                            exp, lines));
289             }
290         }
291 
292         assert(thrown, "The expression did not throw an Error.");
293     }
294 }
295 
296 unittest
297 {
298     // Testing assertErrorStringContains itself
299 
300     import std.exception : assertNotThrown, assertThrown;
301     import std.format : format;
302 
303     // This test requires that bounds checking is active
304     int[] arr;
305     const i = arr.length;
306     auto dg = { ++arr[i]; };    // Intentionally buggy
307 
308     // These should fail because "Does not exist" is not a part of out-of-bound
309     // Error output:
310     assertThrown!Error(assertErrorStringContains(dg, ["Does not exist",
311                                                       "out of bounds"]));
312 
313     assertThrown!Error(assertErrorStringContains(dg, ["out of bounds",
314                                                       "Does not exist"]));
315 
316     // This should pass because all provided texts are parts of out-of-bound
317     // Error output:
318     auto expected = ["out of bounds",
319                      format!"[%s]"(i),
320                      "ArrayIndexError",
321                      format!"array of length %s"(arr.length) ];
322     assertNotThrown!Error(assertErrorStringContains(dg, expected));
323 }
324 
325 unittest
326 {
327     // Test that large data is caught at compile time
328 
329     import std.format : format;
330     import std.meta : AliasSeq;
331 
332     enum size = theError!"foo".data_.length;
333 
334     // Pairs of static arrays of various sizes and whether data should fit
335     enum minAlignment = (void*).alignof;
336     alias cases = AliasSeq!(ubyte[size / 2 - minAlignment], true,
337                             ubyte[size - minAlignment], true,
338                             ubyte[size + 1], false,
339                             );
340 
341     static foreach (i; 0 .. cases.length)
342     {
343         static if (i % 2 == 0)
344         {
345             static assert(
346                 __traits(compiles, fooError("message", cases[i].init)) == cases[i + 1],
347                 format!"Failed for i: %s, type: %s"(i, cases[i].stringof));
348         }
349     }
350 }