1 /** Serialization.
2  *
3  * Test: dmd -version=show -preview=dip1000 -preview=in -vcolumns -d -I.. -i -debug -g -checkaction=context -allinst -unittest -main -run serialization.d
4  * Test: ldmd2 -d -fsanitize=address -I.. -i -debug -g -checkaction=context -allinst -unittest -main -run serialization.d
5  * Debug: ldmd2 -d -fsanitize=address -I.. -i -debug -g -checkaction=context -allinst -unittest -main serialization.d && lldb serialization
6  *
7  * TODO: Detect and pack consecutive bitfields using new `__traits(isBitfield, T)`
8  *
9  * TODO: Give compile-time error message when trying to serialize `void*` but not `void[]`
10  *
11  * TODO: Allocator supoprt
12  *
13  * TODO: Disable (de)serialization of nested types via `!__traits(isNested, T)`
14  *
15  * TODO: Support serialization of cycles and remove `Code.failureCycle` and `sc`.
16  *
17  * TODO: Use direct field setting for T only when __traits(isPOD, T) is true
18          otherwise use __traits(getOverloads, T, "__ctor").
19 		 Try to use this to generalize (de)serialization of `std.json.JSONValue`
20 		 to a type-agnostic logic inside inside generic main generic `serializeRaw`
21 		 and `deserializeRaw`.
22  *
23  * TODO: Support bit-blitting of unions of only non-pointer fields.
24  *
25  * TODO: Only disable union's that contain any pointers.
26  *       Detect using __traits, std.traits or gc_traits.d.
27  *
28  * TODO: Avoid call to `new` when deserializing arrays of immutable elements
29  *       (and perhaps classes) when `Sink` element type `E` is immutable.
30  *
31  * TODO: Exercise `JSONValue` (de)serialization with `nxt.sampling`.
32  *
33  * TODO: Optimize (de)serialization when `__traits(hasIndirections)` is avaiable and
34          `__traits(hasIndirections, T)` is false and `enablesSlicing` is set.
35  */
36 module nxt.serialization;
37 
38 import nxt.visiting : Addresses;
39 import nxt.dip_traits : hasPreviewBitfields;
40 
41 version = serialization_json_test;
42 
43 /++ Serialization format.
44  +/
45 @safe struct Format {
46 	/++ Flag that integral types are packed via variable length encoding (VLE).
47      +/
48 	bool packIntegrals = false;
49 
50 	/++ Flag that scalar types are serialized in native (platform-dependent) byte-order.
51 		Reason for settings this is usually to gain speed.
52      +/
53 	bool useNativeByteOrder = false;
54 
55 	/++ Returns: `true` iff `this` enables array slices of a scalar type
56 		to be read/written without a loop, resulting in higher performance.
57 		+/
58 	@property bool enablesSlicing() const pure nothrow @nogc
59 		=> (!packIntegrals && useNativeByteOrder);
60 }
61 
62 /++ Status.
63 	Converts to `bool true` for failures to simplify control flow in
64     (de)serialization functions.
65  +/
66 struct Status {
67 	/++ Status code. +/
68 	enum Code {
69 		successful = 0,
70 		failure = 1,
71 		failureCycle = 2,
72 	}
73 	Code code;
74 	alias code this;
75 	bool opCast(T : bool)() const nothrow @nogc => code != Code.successful;
76 }
77 
78 /++ Code (unit) type.
79  +/
80 alias CodeUnitType = ubyte;
81 
82 /++ Raw (binary) serialize `arg` to `sink` in format `fmt`.
83 	TODO: Predict `initialAddrsCapacity` in callers.
84  +/
85 Status serializeRaw(T, Sink)(scope ref Sink sink, in T arg, in Format fmt = Format.init, in size_t initialAddrsCapacity = 0) {
86 	scope Addresses addrs;
87 	() @trusted {
88 		addrs.reserve(initialAddrsCapacity); // .reserve should be @trusted here
89 	}();
90 	return serializeRaw_!(T, Sink)(sink, arg, addrs, fmt);
91 }
92 
93 private Status serializeRaw_(T, Sink)(scope ref Sink sink, in T arg, scope ref Addresses addrs, in Format fmt = Format.init) {
94 	alias E = typeof(Sink.init[][0]); // code unit (element) type
95 	static assert(__traits(isUnsigned, E),
96 				   "Non-unsigned sink code unit (element) type " ~ E.stringof);
97 	static assert(!is(T == union),
98 				   "Cannot serialize union type `" ~ T.stringof ~ "`");
99 	static if (is(T == struct) || is(T == union) || is(T == class)) {
100 		Status serializeFields() {
101 			import std.traits : FieldNameTuple;
102 			foreach (fieldName; FieldNameTuple!T)
103 				if (const st = serializeRaw_(sink, __traits(getMember, arg, fieldName), addrs, fmt))
104 					return st;
105 			return Status(Status.Code.successful);
106 		}
107 	}
108 	static if (is(T : __vector(U[N]), U, size_t N)) { // must come before isArithmetic
109 		foreach (const ref elt; arg)
110 			if (const st = serializeRaw_(sink, elt, addrs, fmt)) { return st; }
111     } else static if (__traits(isArithmetic, T)) {
112 		static if (__traits(isIntegral, T)) {
113 			static if (__traits(isUnsigned, T)) {
114 				if (fmt.packIntegrals) {
115 					if (arg < unsignedPrefixSentinel) {
116 						const tmp = cast(E)arg;
117 						assert(tmp != unsignedPrefixSentinel);
118 						sink ~= tmp; // pack in single code unit
119 						return Status(Status.Code.successful);
120 					}
121 					else
122 						sink ~= unsignedPrefixSentinel;
123 				}
124 			} else { // isSigned
125 				if (fmt.packIntegrals) {
126 					if (arg >= byte.min+1 && arg <= byte.max) {
127 						const tmp = cast(byte)arg; // pack in single code unit
128 						assert(tmp != signedPrefixSentinel);
129 						() @trusted { sink ~= ((cast(E*)&tmp)[0 .. 1]); }();
130 						return Status(Status.Code.successful);
131 					}
132 					else
133 						sink ~= signedPrefixSentinel; // signed prefix
134 				}
135 			}
136 		} else static if (__traits(isFloating, T) && is(T : real)) {
137 			/+ TODO: pack small values +/
138 		}
139 		static if (T.sizeof <= 8 && canSwapEndianness!(T)) {
140 			import std.bitmanip : nativeToBigEndian;
141 			if (!fmt.useNativeByteOrder) {
142 				sink ~= arg.nativeToBigEndian[];
143 				return Status(Status.Code.successful);
144 			}
145 		}
146 		// `T` for `T.sizeof == 1` or `T` being `real`:
147 		() @trusted { sink ~= ((cast(E*)&arg)[0 .. T.sizeof]); }();
148     } else static if (is(T == struct) || is(T == union)) {
149 		static if (is(typeof(T.init[]) == U[], U)) { // hasSlicing
150 			if (const st = serializeRaw_(sink, arg[], addrs, fmt)) { return st; }
151 		} else {
152 			if (const st = serializeFields()) { return st; }
153 		}
154     } else static if (is(T == class) || is(T == U*, U)) { // isAddress
155         const bool isNull = arg is null;
156         if (const st = serializeRaw_(sink, isNull, addrs, fmt)) { return st; }
157         if (isNull)
158 			return Status(Status.Code.successful);
159 		import nxt.algorithm.searching : canFind;
160 		void* addr;
161 		() @trusted { addr = cast(void*)arg; }();
162 		if (addrs.canFind(addr)) {
163 			// dbg("Cycle detected at `" ~ T.stringof ~ "`");
164 			return Status(Status.Code.failureCycle);
165 		}
166 		() @trusted { addrs ~= addr; }(); // `addrs`.lifetime <= `arg`.lifetime
167         static if (is(T == class)) {
168 			if (const st = serializeFields()) { return st; }
169         } else {
170 			if (const st = serializeRaw_(sink, *arg, addrs, fmt)) { return st; }
171         }
172     } else static if (is(T U : U[])) { // isArray
173 		static if (!__traits(isStaticArray, T)) {
174 			if (const st = serializeRaw_(sink, arg.length, addrs, fmt)) { return st; }
175 		}
176 		static if (__traits(isScalar, U)) {
177 			if (fmt.enablesSlicing) {
178 				() @trusted { sink ~= ((cast(E*)arg.ptr)[0 .. arg.length*U.sizeof]); }();
179 				return Status(Status.Code.successful);
180 			}
181 		}
182 		static if (is(immutable U == immutable void)) {
183 			ubyte[] raw;
184 			() @trusted { raw = cast(typeof(raw))arg; }();
185 			if (const st = serializeRaw_(sink, raw, addrs, fmt)) { return st; }
186 		} else {
187 			foreach (const ref elt; arg)
188 				if (const st = serializeRaw_(sink, elt, addrs, fmt)) { return st; }
189 		}
190     } else static if (__traits(isAssociativeArray, T)) {
191 		if (const st = serializeRaw_(sink, arg.length, addrs, fmt)) { return st; }
192 		foreach (const ref elt; arg.byKeyValue) {
193 			if (const st = serializeRaw_(sink, elt.key, addrs, fmt)) { return st; }
194 			if (const st = serializeRaw_(sink, elt.value, addrs, fmt)) { return st; }
195 		}
196     } else
197         static assert(0, "Cannot serialize `arg` of type `" ~ T.stringof ~ "`");
198 	return Status(Status.Code.successful);
199 }
200 
201 /++ Raw (binary) deserialize `arg` from `sink` in format `fmt`.
202  +/
203 Status deserializeRaw(T, Sink)(scope ref Sink sink, ref T arg, in Format fmt = Format.init) {
204 	alias E = typeof(Sink.init[][0]); // code unit (element) type
205 	static assert(__traits(isUnsigned, E),
206 				   "Non-unsigned sink code unit (element) type " ~ E.stringof);
207 	static assert(!is(T == union),
208 				   "Cannot deserialize union type `" ~ T.stringof ~ "`");
209 	static if (__traits(hasMember, Sink, "data") &&
210 			   is(immutable typeof(sink.data) == immutable E[])) {
211 		auto data = sink.data; // l-value to pass by ref
212 		const st_ = deserializeRaw!(T)(data, arg, fmt); // Appender arg => arg.data
213 		sink = Sink(data); /+ TODO: avoid this because it allocates +/
214 		return st_;
215 	}
216 	import std.traits : Unqual;
217 	Unqual!T* argP;
218 	() @trusted { argP = cast(Unqual!T*)&arg; }();
219 	static if (is(T == struct) || is(T == union) || is(T == class)) {
220 		import std.traits : FieldNameTuple;
221 		Status deserializeFields() {
222 			foreach (fieldName; FieldNameTuple!T) {
223 				/+ TODO: maybe use *argP here instead: +/
224 				if (const st = deserializeRaw(sink, __traits(getMember, arg, fieldName), fmt)) { return st; }
225 			}
226 			return Status(Status.Code.successful);
227 		}
228 	}
229 	static if (is(T : __vector(U[N]), U, size_t N)) { // must come before isArithmetic
230 		foreach (const ref elt; arg)
231 			if (const st = deserializeRaw(sink, elt, fmt)) { return st; }
232     } else static if (__traits(isArithmetic, T)) {
233 		static if (__traits(isIntegral, T)) {
234 			if (fmt.packIntegrals) {
235 				auto tmp = sink.frontPop!(E)();
236 				static if (__traits(isUnsigned, T)) {
237 					if (tmp != unsignedPrefixSentinel) {
238 						*argP = cast(T)tmp;
239 						return Status(Status.Code.successful);
240 					}
241 				} else {
242 					if (tmp != signedPrefixSentinel) {
243 						() @trusted {*argP = cast(T)*cast(byte*)&tmp;}(); // reinterpret
244 						return Status(Status.Code.successful);
245 					}
246 				}
247 			}
248 		} else static if (__traits(isFloating, T) && is(T : real)) {
249 			/+ TODO: unpack small values +/
250 		}
251 		static if (T.sizeof <= 8 && canSwapEndianness!(T)) {
252 			if (!fmt.useNativeByteOrder) {
253 				*argP = sink.frontPopSwapEndian!(T)();
254 				return Status(Status.Code.successful);
255 			}
256 		}
257 		// `T` for `T.sizeof == 1` or `T` being `real`:
258 		*argP = sink.frontPop!(T)();
259     } else static if (is(T == struct) || is(T == union)) {
260 		static if (is(typeof(T.init[]) == U[], U)) { // hasSlicing
261 			U[] tmp; // T was serialized via `T.opSlice`
262 			if (const st = deserializeRaw(sink, tmp, fmt)) { return st; }
263 			arg = T(tmp);
264 		} else {
265 			if (const st = deserializeFields()) { return st; }
266 		}
267     } else static if (is(T == class) || is(T == U*, U)) { // isAddress
268         bool isNull;
269         if (const st = deserializeRaw(sink, isNull, fmt)) { return st; }
270         if (isNull) {
271 			arg = null;
272 			return Status(Status.Code.successful);
273 		}
274         static if (is(T == class)) {
275 			if (arg is null) {
276 				alias ctors = typeof(__traits(getOverloads, TestClass, "__ctor"));
277 				static assert(ctors.length <= 1, "Cannot deserialize `arg` of type `" ~ T.stringof ~ "` as it has multiple constructors");
278 				static if (ctors.length == 1) {
279 					import std.traits : ParameterTypeTuple;
280 					alias CtorParams = ParameterTypeTuple!(ctors[0]);
281 					CtorParams params;
282 					// pragma(msg, __FILE__, "(", __LINE__, ",1): Debug: ", CtorParams);
283 					/+ TODO: Somehow deserialize `CtorParams` and pass them to constructor probably when compiler has native support for passing tuples to functions. +/
284 					static if (is(typeof(() @safe pure { return new T(params); }))) {
285 						arg = new T();
286 					} else {
287 						static assert(0, "Cannot deserialize `arg` of type `" ~ T.stringof ~ "` as it has no default constructor");
288 					}
289     			} else {
290 					static if (is(typeof(() @safe pure { return new T(); }))) {
291 						arg = new T();
292 					} else {
293 						static assert(0, "Cannot deserialize `arg` of type `" ~ T.stringof ~ "` as it has no default constructor");
294 					}
295 				}
296 			}
297 			if (const st = deserializeFields()) { return st; }
298         } else {
299 			if (arg is null)
300 				arg = new typeof(*T.init);
301             if (const st = deserializeRaw(sink, *arg, fmt)) { return st; }
302         }
303     } else static if (is(T U : U[])) { // isArray
304 		static if (!__traits(isStaticArray, T)) {
305 			typeof(T.init.length) length;
306 			if (const st = deserializeRaw(sink, length, fmt)) { return st; }
307 			/+ TODO: avoid allocation if `E` is `immutable` and `U` is `immutable` and both have .sizeof 1: +/
308 			arg.length = length; // allocates. TODO: use allocator
309 		}
310 		static if (__traits(isScalar, U)) {
311 			if (fmt.enablesSlicing) {
312 				() @trusted { arg = (cast(U*)sink[].ptr)[0 .. arg.length]; }();
313 				sink = cast(Sink)(sink[][arg.length * U.sizeof .. $]);
314 				return Status(Status.Code.successful);
315 			}
316 		}
317         foreach (ref elt; arg)
318             if (const st = deserializeRaw(sink, elt, fmt)) { return st; }
319     } else static if (__traits(isAssociativeArray, T)) {
320 		typeof(T.init.length) length;
321 		if (const st = deserializeRaw(sink, length, fmt)) { return st; }
322 		/+ TODO: isMap: arg.capacity = length; or arg.reserve(length); +/
323 		foreach (_; 0 .. length) {
324 			/* WARNING: `key` and `value` must not be put in outer scope as
325                that will lead to keys being overwritten. */
326 			typeof(T.init.keys[0]) key;
327 			typeof(T.init.values[0]) value;
328 			if (const st = deserializeRaw(sink, key, fmt)) { return st; }
329 			if (const st = deserializeRaw(sink, value, fmt)) { return st; }
330 			arg[key] = value;
331 		}
332     } else
333         static assert(0, "Cannot deserialize `arg` of type `" ~ T.stringof ~ "`");
334 	return Status(Status.Code.successful);
335 }
336 
337 private static immutable CodeUnitType unsignedPrefixSentinel = 0b_1111_1111;
338 private static immutable CodeUnitType   signedPrefixSentinel = 0b_1000_0000;
339 
340 private T frontPop(T, Sink)(ref Sink sink) in(T.sizeof <= sink[].length) {
341 	T* ptr;
342 	() @trusted { ptr = (cast(T*)sink[][0 .. T.sizeof]); }();
343 	typeof(return) result = *ptr; /+ TODO: unaligned access +/
344 	sink = cast(Sink)(sink[][T.sizeof .. $]);
345 	return result;
346 }
347 
348 private T frontPopSwapEndian(T, Sink)(ref Sink sink) if (T.sizeof >= 2) {
349 	enum sz = T.sizeof;
350 	import std.bitmanip : bigEndianToNative;
351 	typeof(return) result = sink[][0 .. sz].bigEndianToNative!(T, sz); /+ TODO: unaligned access +/
352 	sink = cast(Sink)(sink[][sz .. $]);
353 	return result;
354 }
355 
356 /++ Is true iff `T` has swappable endianness (byte-order). +/
357 private enum canSwapEndianness(T) = (T.sizeof >= 2 && T.sizeof <= 8 && __traits(isArithmetic, T));
358 
359 /// enum both sink type to trigger instantiation
360 version (none)
361 @safe pure nothrow unittest {
362 	foreach (const packIntegrals; [false, true]) {
363 		foreach (const useNativeByteOrder; [false, true]) {
364 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
365 			static foreach (Sink; AliasSeq!(ArraySink, AppenderSink)) {{ // trigger instantiation
366 				Sink sink;
367 				alias T = void[];
368 				T t = [1,2,3,4];
369 				assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
370 				// assert(sink[].length == (packIntegrals ? 1 : T.sizeof));
371 				// T u;
372 				// assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
373 				// assert(sink[].length == 0);
374 				// assert(t == u);
375 			}}
376 		}
377 	}
378 }
379 
380 /// enum both sink type to trigger instantiation
381 @safe pure nothrow unittest {
382 	foreach (const packIntegrals; [false, true]) {
383 		foreach (const useNativeByteOrder; [false, true]) {
384 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
385 			static foreach (Sink; AliasSeq!(ArraySink, AppenderSink)) {{ // trigger instantiation
386 				Sink sink;
387 				alias T = TestEnum;
388 				T t;
389 				assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
390 				assert(sink[].length == (packIntegrals ? 1 : T.sizeof));
391 				T u;
392 				assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
393 				assert(sink[].length == 0);
394 				assert(t == u);
395 			}}
396 		}
397 	}
398 }
399 
400 /// empty {struct|union}
401 @safe pure nothrow unittest {
402 	foreach (const packIntegrals; [false, true]) {
403 		foreach (const useNativeByteOrder; [false, true]) {
404 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
405 			AppenderSink sink;
406 			struct S {}
407 			struct U {}
408 			static foreach (T; AliasSeq!(S, U)) {{
409 				T t;
410 				assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
411 				assert(sink[].length == 0);
412 				T u;
413 				assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
414 				assert(sink[].length == 0);
415 				static if (!is(T == class)) {
416 					assert(t == u);
417 				}
418 			}}
419 		}
420 	}
421 }
422 
423 /// class type
424 @safe pure nothrow unittest {
425 	foreach (const packIntegrals; [false, true]) {
426 		foreach (const useNativeByteOrder; [false, true]) {
427 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
428 			AppenderSink sink;
429 			alias T = TestClass;
430 			T t = new T(11,22);
431 			assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
432 			assert(sink[].length != 0);
433 			if (fmt.packIntegrals)
434 				assert(sink[] == [0, 11, 22]);
435 			T u;
436 			assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
437 			assert(sink[].length == 0);
438 			assert(t.tupleof == u.tupleof);
439 		}
440 	}
441 }
442 
443 /// cycle-struct type
444 version (none) /+ TODO: activate +/
445 @trusted unittest {
446 	foreach (const packIntegrals; [false, true]) {
447 		foreach (const useNativeByteOrder; [false, true]) {
448 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
449 			AppenderSink sink;
450 			alias T = CycleStruct;
451 			T t = new T();
452 			t.x = 42;
453 			() @trusted {
454 				t.parent = &t;
455 			}();
456 			assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
457 			assert(sink[].length != 0);
458 			if (fmt.packIntegrals)
459 				assert(sink == [0, 11, 22]);
460 			T u;
461 			assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
462 			assert(sink[].length == 0);
463 			assert(t.tupleof == u.tupleof);
464 		}
465 	}
466 }
467 
468 /// cycle-class type
469 @trusted unittest {
470 	foreach (const packIntegrals; [false, true]) {
471 		foreach (const useNativeByteOrder; [false, true]) {
472 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
473 			AppenderSink sink;
474 			alias T = CycleClass;
475 			T t = new T(42);
476 			assert(sink.serializeRaw(t, fmt) == Status(Status.Code.failureCycle));
477 			assert(sink[].length != 0);
478 			/+ TODO: activate when cycles are supported: +/
479 			// T u;
480 			// assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
481 			// assert(sink[].length == 0);
482 			// assert(t.tupleof == u.tupleof);
483 		}
484 	}
485 }
486 
487 /// struct with static field
488 @safe pure nothrow unittest {
489 	foreach (const packIntegrals; [false, true]) {
490 		foreach (const useNativeByteOrder; [false, true]) {
491 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
492 			AppenderSink sink;
493 			struct T { static int _; }
494 			T t;
495 			assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
496 			assert(sink[].length == 0);
497 			T u;
498 			assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
499 			assert(sink[].length == 0);
500 			assert(t == u);
501 		}
502 	}
503 }
504 
505 /// struct with immutable field
506 @safe pure nothrow unittest {
507 	foreach (const packIntegrals; [false, true]) {
508 		foreach (const useNativeByteOrder; [false, true]) {
509 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
510 			AppenderSink sink;
511 			struct T { immutable int _; }
512 			T t;
513 			assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
514 			assert(sink[].length == (packIntegrals ? 1 : T.sizeof));
515 			T u;
516 			assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
517 			assert(sink[].length == 0);
518 			assert(t == u);
519 		}
520 	}
521 }
522 
523 /// struct with bitfields
524 @safe pure nothrow unittest {
525 	static if (hasPreviewBitfields) {
526 		foreach (const packIntegrals; [false, true]) {
527 			foreach (const useNativeByteOrder; [false, true]) {
528 				const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
529 				AppenderSink sink;
530 				alias W = ubyte;
531 				struct T { W b_0_2 : 2; W b_2_6 : 6; }
532 				static assert(T.sizeof == W.sizeof);
533 				T t;
534 				assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
535 				assert(sink[].length == (packIntegrals ? 2 * W.sizeof : 2*T.sizeof));
536 				T u;
537 				assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
538 				assert(sink[].length == 0);
539 				assert(t == u);
540 			}
541 		}
542 	}
543 }
544 
545 /// {char|wchar|dchar}
546 @safe pure nothrow unittest {
547 	foreach (const packIntegrals; [false, true]) {
548 		foreach (const useNativeByteOrder; [false, true]) {
549 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
550 			AppenderSink sink;
551 			static foreach (T; CharTypes) {{
552 				foreach (const T t; 0 .. 127+1) {
553 					assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
554 					assert(sink[].length == (packIntegrals ? 1 : T.sizeof));
555 					T u;
556 					assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
557 					assert(sink[].length == 0);
558 					assert(t == u);
559 				}
560 			}}
561 		}
562 	}
563 }
564 
565 /// {char|wchar|dchar}[]
566 @safe pure nothrow unittest {
567 	foreach (const packIntegrals; [false, true]) {
568 		foreach (const useNativeByteOrder; [false, true]) {
569 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
570 			AppenderSink sink;
571 			import std.meta : AliasSeq;
572 			static foreach (E; CharTypes) {{
573 				alias T = E[];
574 				T t;
575 				assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
576 				assert(sink[].length != 0);
577 				T u;
578 				assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
579 				assert(sink[].length == 0);
580 				assert(t == u);
581 			}}
582 		}
583 	}
584 }
585 
586 /// signed integral
587 @safe pure nothrow unittest {
588 	foreach (const packIntegrals; [false, true]) {
589 		foreach (const useNativeByteOrder; [false, true]) {
590 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
591 			AppenderSink sink;
592 			import std.meta : AliasSeq;
593 			static foreach (T; AliasSeq!(byte, short, int, long)) {{
594 				foreach (const T t; byte.min+1 .. byte.max+1) {
595 					assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
596 					assert(sink[].length == (packIntegrals ? 1 : T.sizeof));
597 					T u;
598 					assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
599 					assert(sink[].length == 0);
600 					assert(t == u);
601 				}
602 			}}
603 		}
604 	}
605 }
606 
607 /// core.simd vector types
608 @safe pure nothrow unittest {
609 	foreach (const packIntegrals; [false, true]) {
610 		foreach (const useNativeByteOrder; [false, true]) {
611 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
612 			AppenderSink sink;
613 			import std.meta : AliasSeq;
614 			import core.simd;
615 			/+ TODO: support more core.simd types +/
616 			static foreach (T; AliasSeq!(byte16, float4, double2)) {{
617 				T t = 0;
618 				assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
619 				assert(sink[].length == T.sizeof);
620 				T u;
621 				assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
622 				assert(sink[].length == 0);
623 				assert(t[] == u[]);
624 			}}
625 		}
626 	}
627 }
628 
629 /// unsigned integral
630 @safe pure nothrow unittest {
631 	foreach (const packIntegrals; [false, true]) {
632 		foreach (const useNativeByteOrder; [false, true]) {
633 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
634 			AppenderSink sink;
635 			import std.meta : AliasSeq;
636 			static foreach (T; AliasSeq!(ubyte, ushort, uint, ulong)) {{
637 				foreach (const T t; 0 .. ubyte.max) {
638 					assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
639 					assert(sink[].length == (packIntegrals ? 1 : T.sizeof));
640 					T u;
641 					assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
642 					assert(sink[].length == 0);
643 					assert(t == u);
644 				}
645 			}}
646 		}
647 	}
648 }
649 
650 /// floating point
651 @safe pure nothrow unittest {
652 	foreach (const packIntegrals; [false, true]) {
653 		foreach (const useNativeByteOrder; [false, true]) {
654 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
655 			AppenderSink sink;
656 			import std.meta : AliasSeq;
657 			static foreach (T; AliasSeq!(float, double, real)) {{
658 				foreach (const T t; 0 .. ubyte.max) {
659 					assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
660 					assert(sink[].length == (packIntegrals ? T.sizeof : T.sizeof));
661 					T u;
662 					assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
663 					assert(sink[].length == 0);
664 					assert(t == u);
665 				}
666 			}}
667 		}
668 	}
669 }
670 
671 /// integral pointer
672 @trusted pure nothrow unittest {
673 	foreach (const packIntegrals; [false, true]) {
674 		foreach (const useNativeByteOrder; [false, true]) {
675 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
676 			AppenderSink sink;
677 			import std.meta : AliasSeq;
678 			static foreach (E; AliasSeq!(uint, ulong)) {{
679 				E val = 42;
680 				struct T { E* p1, p2; }
681 				T t = T(null,&val);
682 				assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
683 				assert(sink[].length != 0);
684 				T u;
685 				assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
686 				assert(sink[].length == 0);
687 				assert(t.p1 is u.p1);
688 				assert(*t.p2 == *u.p2);
689 			}}
690 		}
691 	}
692 }
693 
694 /// static array
695 @safe pure nothrow unittest {
696 	foreach (const packIntegrals; [false, true]) {
697 		foreach (const useNativeByteOrder; [false, true]) {
698 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
699 			AppenderSink sink;
700 			enum n = 3;
701 			alias T = int[n];
702 			T t = [11,22,33];
703 			assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
704 			assert(sink[].length != 0);
705 			T u;
706 			assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
707 			assert(sink[].length == 0);
708 			assert(t == u);
709 		}
710 	}
711 }
712 
713 /// dynamic array
714 @safe pure nothrow unittest {
715 	foreach (const packIntegrals; [false, true]) {
716 		foreach (const useNativeByteOrder; [false, true]) {
717 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
718 			AppenderSink sink;
719 			alias T = int[];
720 			T t = [11,22,33];
721 			assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
722 			assert(sink[].length == (fmt.packIntegrals ? (1 + t.length) : 20));
723 			T u;
724 			assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
725 			assert(sink[].length == 0);
726 
727 			assert(t == u);
728 		}
729 	}
730 }
731 
732 /// associative array
733 @safe pure nothrow unittest {
734 	foreach (const packIntegrals; [false, true]) {
735 		foreach (const useNativeByteOrder; [false, true]) {
736 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
737 			AppenderSink sink;
738 			alias T = int[int];
739 			T t = [1: 1, 2: 2];
740 			assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
741 			assert(sink[].length != 0);
742 			T u;
743 			assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
744 			assert(sink[].length == 0);
745 			assert(t == u);
746 		}
747 	}
748 }
749 
750 /// associative array
751 @safe pure nothrow unittest {
752 	foreach (const packIntegrals; [false, true]) {
753 		foreach (const useNativeByteOrder; [false, true]) {
754 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
755 			AppenderSink sink;
756 			alias T = int[string];
757 			T t = ["1": 3, "2": 4];
758 			assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
759 			assert(sink[].length != 0);
760 			T u = T.init;
761 			assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
762 			assert(sink[].length == 0);
763 			assert(t == u);
764 		}
765 	}
766 }
767 
768 /// empty `std.array.Appender` via slicing
769 @safe pure nothrow unittest {
770 	foreach (const packIntegrals; [false, true]) {
771 		foreach (const useNativeByteOrder; [false, true]) {
772 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
773 			import std.array : Appender;
774 			AppenderSink sink;
775 			alias A = int[];
776 			alias T = Appender!(A);
777 			T t = [];
778 			assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
779 			assert(sink[].length == (fmt.packIntegrals ? 1 : 8) + t.data.length);
780 			T u;
781 			assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
782 			assert(sink[].length == 0);
783 			assert(t[] == u[]);
784 		}
785 	}
786 }
787 
788 /// populated `std.array.Appender` via slicing
789 @safe pure nothrow unittest {
790 	foreach (const packIntegrals; [false, true]) {
791 		foreach (const useNativeByteOrder; [false, true]) {
792 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
793 			import std.array : Appender;
794 			AppenderSink sink;
795 			alias A = int[];
796 			alias T = Appender!(A);
797 			A a = [11,22,33];
798 			T t = a;
799 			assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
800 			assert(sink[].length == (fmt.packIntegrals ? 4 : 20));
801 			AppenderSink asink;
802 			assert(!asink.serializeRaw(a, fmt));
803 			assert(asink[].length == (fmt.packIntegrals ? 4 : 20));
804 			assert(sink[] == asink[]);
805 			T u;
806 			assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
807 			assert(sink[].length == 0);
808 			assert(t[] == u[]);
809 		}
810 	}
811 }
812 
813 /// aggregate type
814 @safe pure nothrow unittest {
815 	foreach (const packIntegrals; [false, true]) {
816 		foreach (const useNativeByteOrder; [false, true]) {
817 			const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
818 			AppenderSink sink;
819 			struct P { int x, y; int* p1, p2; char[3] ch3; char ch; wchar wc; dchar dc; int[int] aa; }
820 			struct T { int x, y; long l; float f; double d; real r; bool b1, b2; P p; }
821 			T t = T(0x01234567,0x76543210, 0x01234567_01234567, 3.14,3.14,3.14, false,true,
822 					P(2,3, null,null, "abc", 'a', 'b', 'c', [11:-11,-22:22,-33:-33]));
823 			assert(sink.serializeRaw(t, fmt) == Status(Status.Code.successful));
824 			T u;
825 			assert(sink.deserializeRaw(u, fmt) == Status(Status.Code.successful));
826 			assert(sink[].length == 0);
827 			assert(t == u);
828 		}
829 	}
830 }
831 
832 version (serialization_json_test)
833 private import std.json : JSONValue, JSONType, parseJSON;
834 
835 /++ Raw (binary) serialize `JSONValue arg` to `sink` in format `fmt`. +/
836 version (serialization_json_test)
837 private Status serializeRaw_(T : JSONValue, AppenderSink)(scope ref AppenderSink sink, in T arg, scope ref Addresses addrs, in Format fmt = Format.init) {
838 	if (const st = serializeRaw_(sink, arg.type, addrs, fmt)) { return st; }
839 	final switch (arg.type) {
840     case JSONType.integer:
841 		return serializeRaw_(sink, arg.integer, addrs, fmt);
842     case JSONType.uinteger:
843 		return serializeRaw_(sink, arg.uinteger, addrs, fmt);
844     case JSONType.float_:
845 		return serializeRaw_(sink, arg.floating, addrs, fmt);
846     case JSONType..string:
847 		return serializeRaw_(sink, arg.str, addrs, fmt);
848     case JSONType.object:
849 		return serializeRaw_(sink, arg.object, addrs, fmt);
850     case JSONType.array:
851 		return serializeRaw_(sink, arg.array, addrs, fmt);
852     case JSONType.true_:
853     case JSONType.false_:
854     case JSONType.null_:
855 		return Status(Status.Code.successful);
856     }
857 }
858 
859 /++ Raw (binary) deserialize `JSONValue arg` from `sink` in format `fmt`. +/
860 version (serialization_json_test)
861 Status deserializeRaw(T : JSONValue, AppenderSink)(scope ref AppenderSink sink, ref T arg, in Format fmt = Format.init) {
862 	JSONType type;
863 	if (const st = deserializeRaw(sink, type, fmt)) { return st; }
864 	typeof(return) st;
865 	final switch (type) {
866     case JSONType.integer:
867 		typeof(arg.integer) value;
868 		st = deserializeRaw(sink, value, fmt);
869 		if (st == Status(Status.Code.successful)) arg = JSONValue(value);
870 		break;
871     case JSONType.uinteger:
872 		typeof(arg.uinteger) value;
873 		st = deserializeRaw(sink, value, fmt);
874 		if (st == Status(Status.Code.successful)) arg = JSONValue(value);
875 		break;
876     case JSONType.float_:
877 		typeof(arg.floating) value;
878 		st = deserializeRaw(sink, value, fmt);
879 		if (st == Status(Status.Code.successful)) arg = JSONValue(value);
880 		break;
881     case JSONType..string:
882 		typeof(arg.str) value;
883 		st = deserializeRaw(sink, value, fmt);
884 		if (st == Status(Status.Code.successful)) arg = JSONValue(value);
885 		break;
886     case JSONType.object:
887 		typeof(arg.object) value;
888 		st = deserializeRaw(sink, value, fmt);
889 		if (st == Status(Status.Code.successful)) arg = JSONValue(value);
890 		break;
891     case JSONType.array:
892 		typeof(arg.array) value;
893 		st = deserializeRaw(sink, value, fmt);
894 		if (st == Status(Status.Code.successful)) arg = JSONValue(value);
895 		break;
896     case JSONType.true_:
897 		arg = true;
898 		st = Status(Status.Code.successful);
899 		break;
900     case JSONType.false_:
901 		arg = false;
902 		st = Status(Status.Code.successful);
903 		break;
904     case JSONType.null_:
905 		arg = null;
906 		st = Status(Status.Code.successful);
907 		break;
908     }
909 	return st;
910 }
911 
912 // { "optional": true }
913 version (serialization_json_test)
914 @trusted unittest {
915 	foreach (const s; [`false`,
916 					   `true`,
917 					   `null`,
918 					   `{}`,
919 					   `12`,
920 					   `"x"`,
921 					   `[1,2]`,
922 					   `[1,2,[3,4,5,"x","y"],"a",null]`,
923 					   `[1,3.14,"x",null]`,
924 					   `{ "optional":false }`,
925 					   `{ "optional":true }`,
926 					   `{ "optional":null }`,
927 					   `{ "a":"a", }`,
928 					   `{ "a":"a", "a":"a", }`,
929 					   `{ "a":1, "a":2, }`,
930 					   `{  "":1,  "":2, }`,
931 					   `{  "":1, "b":2, }`,
932 					   `{ "a":1,  "":2, }`,
933 					   `{ "a":1, "b":2, }`,
934 					   /+ TODO: this fails: readLargeFile, +/
935 	]) {
936 		foreach (const packIntegrals; [false, true]) {
937 			foreach (const useNativeByteOrder; [false, true]) {
938 				const fmt = Format(packIntegrals: packIntegrals, useNativeByteOrder: useNativeByteOrder);
939 				AppenderSink sink;
940 				alias T = JSONValue;
941 				const T t = s.parseJSON();
942 				assert(sink.serializeRaw!(JSONValue)(t, fmt) == Status(Status.Code.successful));
943 				assert(sink[].length != 0);
944 				T u;
945 				assert(sink.deserializeRaw!(JSONValue)(u, fmt) == Status(Status.Code.successful));
946 				assert(sink[].length == 0);
947 				assert(t == u);
948 			}
949 		}
950 	}
951 }
952 
953 version (none)
954 version (serialization_json_test)
955 private @system string readLargeFile() {
956 	import std.path : expandTilde;
957 	import std.file : readText;
958 	return "~/Downloads/large-file.json".expandTilde.readText;
959 }
960 
961 version (unittest) {
962 	import std.array : Appender;
963 	import std.meta : AliasSeq;
964 	private alias ArraySink = CodeUnitType[];
965 	private alias AppenderSink = Appender!(ArraySink);
966 	private alias CharTypes = AliasSeq!(char, wchar, dchar);
967 	private enum TestEnum { first, second, third }
968 	private class TestClass {
969 		int a, b;
970 		this(int a = 0, int b = 0) @safe pure nothrow @nogc {
971 			this.a = a;
972 			this.b = b;
973 		}
974 	}
975 	private class CycleClass {
976 		this(int x = 0) @safe pure nothrow @nogc {
977 			this.x = x;
978 			this.parent = this; // self-reference
979 		}
980 		int x;
981 		CycleClass parent;
982 	}
983 	private class CycleStruct {
984 		int x;
985 		CycleStruct* parent;
986 	}
987 	import std.traits : ParameterTypeTuple;
988 	alias ConstructorParams = ParameterTypeTuple!(typeof(__traits(getOverloads, TestClass, "__ctor")[0]));
989 	static assert(is(ConstructorParams == AliasSeq!(int, int)));
990 	debug import nxt.debugio;
991 }