1 /**
2  * ae.utils.math.mixed_radix
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <ae@cy.md>
12  */
13 
14 module ae.utils.math.mixed_radix;
15 
16 import std.meta : staticIndexOf, staticMap;
17 
18 static if (is(typeof({ import std.sumtype : SumType, match; })))
19 	import std.sumtype : SumType, match;
20 
21 // TODO: Find what this thing is actually called.
22 /// A mixed-radix number coding system.
23 template MixedRadixCoder(
24 	/// Numeric type for decoded items.
25 	I,
26 	/// Numeric type for encoded result.
27 	E,
28 	/// Use an encoding with an explicit end of items.
29 	bool withEOF = false,
30 )
31 {
32 	/// This encoding system is LIFO, so the encoder buffers all items
33 	/// until `.finish` is called.
34 	struct Encoder(
35 		/// Maximum number of encoded items.
36 		size_t maxItems,
37 	)
38 	{
39 	private:
40 		struct Item { I n, card; }
41 		MaybeDynamicArray!(Item, maxItems) items;
42 
43 	public:
44 		/// Encode one item.
45 		/// `card` is the item's cardinality, i.e. one past its maximum.
46 		void put(I n, I card)
47 		in(0 <= n && n < card)
48 		{
49 			items ~= Item(n, card);
50 		}
51 
52 		/// Finish encoding and return the encoded result.
53 		E finish()
54 		{
55 			E result = withEOF ? 1 : 0;
56 			foreach_reverse (ref item; items)
57 			{
58 				result *= item.card;
59 				result += item.n;
60 			}
61 			return result;
62 		}
63 	}
64 
65 	/// As above. This will allocate the items dynamically.
66 	alias VariableLengthEncoder = Encoder!(-1);
67 
68 	/// Like `Encoder`, but does not use a temporary buffer.
69 	/// Instead, the user is expected to put the items in reverse order.
70 	struct RetroEncoder
71 	{
72 	private:
73 		E encoded = withEOF ? 1 : 0;
74 
75 	public:
76 		void put(I n, I card)
77 		in (0 <= n && n < card)
78 		{
79 			encoded *= card;
80 			encoded += n;
81 		} ///
82 
83 		E finish()
84 		{
85 			return encoded;
86 		} ///
87 	}
88 
89 	/// The decoder.
90 	struct Decoder
91 	{
92 	private:
93 		E encoded;
94 
95 	public:
96 		this(E encoded)
97 		{
98 			this.encoded = encoded;
99 			static if (withEOF)
100 				assert(encoded > 0);
101 		} ///
102 
103 		I get(I card)
104 		in(card > 0)
105 		{
106 			I value = encoded % card;
107 			encoded /= card;
108 			static if (withEOF)
109 				assert(encoded > 0, "Decoding error");
110 			return value;
111 		} ///
112 
113 		static if (withEOF)
114 		@property bool empty() const { return encoded == 1; } ///
115 	}
116 }
117 
118 ///
119 debug(ae_unittest) unittest
120 {
121 	alias Coder = MixedRadixCoder!(uint, uint, true);
122 	Coder.Encoder!2 encoder;
123 	encoder.put(5, 8);
124 	encoder.put(1, 2);
125 	auto result = encoder.finish();
126 
127 	auto decoder = Coder.Decoder(result);
128 	assert(decoder.get(8) == 5);
129 	assert(decoder.get(2) == 1);
130 	assert(decoder.empty);
131 }
132 
133 debug(ae_unittest) unittest
134 {
135 	import std.meta : AliasSeq;
136 	import std.traits : EnumMembers;
137 	import std.exception : assertThrown;
138 	import core.exception : AssertError;
139 
140 	alias I = uint;
141 	alias E = uint;
142 
143 	enum Mode { dynamicSize, staticSize, retro }
144 	foreach (mode; EnumMembers!Mode)
145 		foreach (withEOF; AliasSeq!(false, true))
146 		{
147 			void testImpl()
148 			{
149 				alias Coder = MixedRadixCoder!(I, E, withEOF);
150 
151 				static if (mode == Mode.retro)
152 				{
153 					Coder.RetroEncoder encoder;
154 					encoder.put(1, 2);
155 					encoder.put(5, 8);
156 				}
157 				else
158 				{
159 					static if (mode == Mode.dynamicSize)
160 						Coder.VariableLengthEncoder encoder;
161 					else
162 						Coder.Encoder!2 encoder;
163 
164 					encoder.put(5, 8);
165 					encoder.put(1, 2);
166 				}
167 				auto result = encoder.finish();
168 
169 				auto decoder = Coder.Decoder(result);
170 				static if (withEOF) assert(!decoder.empty);
171 				assert(decoder.get(8) == 5);
172 				static if (withEOF) assert(!decoder.empty);
173 				assert(decoder.get(2) == 1);
174 				static if (withEOF) assert(decoder.empty);
175 
176 				static if (withEOF)
177 				{
178 					debug assertThrown!AssertError(decoder.get(42));
179 				}
180 				else
181 					assert(decoder.get(42) == 0);
182 			}
183 			static if (mode == Mode.dynamicSize)
184 				testImpl();
185 			else
186 			{
187 				@nogc void test() { testImpl(); }
188 				test();
189 			}
190 		}
191 }
192 
193 /// Serializes structs and static arrays using a `MixedRadixCoder`.
194 /// Consults types' `.max` property to obtain cardinality.
195 template SerializationCoder(alias Coder, S)
196 {
197 	private alias I = typeof(Coder.Decoder.init.get(0));
198 	private alias E = typeof(Coder.RetroEncoder.init.finish());
199 
200 	private mixin template Visitor(bool serializing, bool deserializing, bool retro)
201 	{
202 		static assert(!(serializing && deserializing));
203 
204 		void visit(T)(ref T value)
205 		{
206 			static if (is(T : I) && is(typeof(T.max) : I))
207 			{
208 				I max = T.max;
209 				I card = max; card++;
210 				assert(card > max, "Overflow");
211 				handleLeaf(value, card);
212 			}
213 			else
214 			static if (is(T == SumType!Types, Types...))
215 			{
216 				alias SubCoder = MixedRadixCoder!(I, E, false); // No EOF
217 
218 				I totalCard = 0;
219 				static foreach (Type; Types)
220 					totalCard += SerializationCoder!(SubCoder, Type).cardinality;
221 
222 				I encoded;
223 				static if (serializing)
224 				{
225 					I handler(FieldType)(ref const FieldType field)
226 					{
227 						I encoded;
228 						enum typeIndex = staticIndexOf!(FieldType, Types);
229 						static assert(typeIndex >= 0);
230 						static foreach (Type; Types[0 .. typeIndex])
231 						{{
232 							E fieldCard = SerializationCoder!(SubCoder, Type).cardinality;
233 							encoded += fieldCard;
234 						}}
235 
236 						auto encodedField = SerializationCoder!(SubCoder, FieldType).serialize(field);
237 						encoded += encodedField;
238 						return encoded;
239 					}
240 					alias handlers = staticMap!(handler, Types);
241 					encoded = value.match!(handlers);
242 				}
243 				handleLeaf(encoded, totalCard);
244 				static if (deserializing)
245 				{
246 					I lowBound;
247 					static foreach (Type; Types)
248 					{{
249 						E fieldCard = SerializationCoder!(SubCoder, Type).cardinality;
250 						I highBound = lowBound; highBound += fieldCard;
251 						if (encoded < highBound)
252 						{
253 							auto encodedField = encoded - lowBound;
254 							value = T(SerializationCoder!(SubCoder, Type).deserialize(encodedField));
255 							return;
256 						}
257 						lowBound = highBound;
258 					}}
259 					assert(false);
260 				}
261 			}
262 			else
263 			static if (is(T == struct))
264 				static if (retro)
265 					foreach_reverse (ref field; value.tupleof)
266 						visit(field);
267 				else
268 					foreach (ref field; value.tupleof)
269 						visit(field);
270 			else
271 			static if (is(T == Q[N], Q, size_t N))
272 				static if (retro)
273 					foreach_reverse (ref item; value)
274 						visit(item);
275 				else
276 					foreach (ref item; value)
277 						visit(item);
278 			else
279 				static assert(false, "Don't know what to do with " ~ T.stringof);
280 		}
281 	}
282 
283 	private struct Serializer
284 	{
285 		Coder.RetroEncoder encoder;
286 		void handleLeaf(I value, I card) { encoder.put(value, card); }
287 		mixin Visitor!(true, false, true);
288 	}
289 
290 	E serialize()(auto ref const S s)
291 	{
292 		Serializer serializer;
293 		serializer.visit(s);
294 		return serializer.encoder.finish();
295 	} ///
296 
297 	private struct Deserializer
298 	{
299 		Coder.Decoder decoder;
300 		void handleLeaf(T)(ref T value, I card) { value = cast(T)decoder.get(card); }
301 		mixin Visitor!(false, true, false);
302 	}
303 
304 	S deserialize(E encoded) nothrow @nogc
305 	{
306 		Deserializer deserializer;
307 		deserializer.decoder = Coder.Decoder(encoded);
308 		S result;
309 		deserializer.visit(result);
310 		static if (__traits(hasMember, deserializer.decoder, "empty"))
311 			assert(deserializer.decoder.empty);
312 		return result;
313 	} ///
314 
315 	private struct MinWriter
316 	{
317 		Coder.RetroEncoder encoder;
318 		void handleLeaf(I /*value*/, I card) { encoder.put(0, card); }
319 		mixin Visitor!(false, false, true);
320 	}
321 
322 	E minValue() nothrow @nogc
323 	{
324 		MinWriter writer;
325 		S s = S.init;
326 		writer.visit(s);
327 		return writer.encoder.finish();
328 	} ///
329 
330 	private struct MaxWriter
331 	{
332 		Coder.RetroEncoder encoder;
333 		void handleLeaf(I /*value*/, I card) { encoder.put(cast(I)(card - 1), card); }
334 		mixin Visitor!(false, false, true);
335 	}
336 
337 	E maxValue() nothrow @nogc
338 	{
339 		MaxWriter writer;
340 		S s = S.init;
341 		writer.visit(s);
342 		return writer.encoder.finish();
343 	} ///
344 
345 	E cardinality() nothrow @nogc { return (maxValue - minValue) + 1; }
346 }
347 
348 ///
349 debug(ae_unittest) unittest
350 {
351 	static struct WithMax(T, T max_)
352 	{
353 		T value;
354 		alias value this;
355 		enum T max = max_;
356 	}
357 
358 	enum E { a, b, c }
359 	alias D6 = WithMax!(uint, 6);
360 
361 	static struct S
362 	{
363 		ubyte a;
364 		bool b;
365 		E[3] e;
366 		D6 d;
367 	}
368 
369 	alias Coder = SerializationCoder!(MixedRadixCoder!(uint, ulong, true), S);
370 	auto s = S(1, true, [E.a, E.b, E.c], D6(4));
371 	auto encoded = Coder.serialize(s);
372 	assert(Coder.deserialize(encoded) == s);
373 	assert(Coder.minValue() <= encoded && encoded <= Coder.maxValue());
374 }
375 
376 static if (is(SumType!int))
377 debug(ae_unittest) unittest
378 {
379 	struct A {}
380 	struct B { bool v; }
381 	struct C {}
382 	alias S = SumType!(A, B, C);
383 
384 	alias Coder = SerializationCoder!(MixedRadixCoder!(uint, ulong, false), S);
385 	static assert(Coder.cardinality == 4);
386 
387 	foreach (s; [S(A()), S(B(false)), S(B(true)), S(C())])
388 		assert(Coder.deserialize(Coder.serialize(s)) == s);
389 }
390 
391 private struct MaybeDynamicArray(T, size_t size = -1)
392 {
393 	static if (size == -1)
394 	{
395 		T[] items;
396 		alias items this;
397 	}
398 	else
399 	{
400 		T[size] items;
401 		size_t length;
402 		void opOpAssign(string op : "~")(T item) { items[length++] = item; }
403 		T[] opSlice() { return items[0 .. length]; }
404 	}
405 }