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