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 }