1 module mecca.lib.typedid;
2 
3 // Licensed under the Boost license. Full copyright information in the AUTHORS file
4 
5 import std.traits;
6 import std.conv;
7 
8 import mecca.lib.exception;
9 import mecca.lib.reflection : PromotedType;
10 import mecca.log : FMT, notrace;
11 
12 template RawTypedIdentifier(string _name, T, T _invalid, T _init, FMT fmt, bool algebraic)
13     if (isIntegral!T)
14 {
15     @fmt
16     struct RawTypedIdentifier {
17     private:
18         T _value = _init;
19 
20         enum Algebraic = algebraic;
21 
22     public:
23         alias UnderlyingType = T;
24         enum RawTypedIdentifier invalid = RawTypedIdentifier(_invalid);
25         enum name = _name;
26 
27         this(T value) @safe pure nothrow @nogc {
28             this._value = value;
29         }
30         // Default this(this) does the right thing
31 
32         // For some reason, this form is not covered by this(this)
33         this(RawTypedIdentifier that) @safe pure nothrow @nogc {
34             this._value = that.value;
35         }
36 
37         this(string str) @safe {
38             setFromString(str);
39         }
40 
41         @property T value() const pure nothrow @safe @nogc {
42             return _value;
43         }
44         @property bool isValid() const pure nothrow @safe @nogc {
45             return _value != _invalid;
46         }
47         // XXX do we want this?
48         ref RawTypedIdentifier opAssign(T val) nothrow @safe @nogc {
49             _value = val;
50             return this;
51         }
52 
53         // Default opEquals does the right thing
54 
55         int opCmp(in RawTypedIdentifier rhs) const pure nothrow @safe @nogc {
56             return value > rhs.value ? 1 : (value < rhs.value ? -1 : 0);
57         }
58 
59         static if (algebraic) {
60             enum RawTypedIdentifier min = T.min;
61             enum RawTypedIdentifier max = T.max;
62 
63             int opCmp(in T rhs) const pure nothrow @safe @nogc {
64                 return opCmp( RawTypedIdentifier(rhs) );
65                 // return value > rhs ? 1 : (value < rhs ? -1 : 0);
66             }
67 
68             // Pointer like semantics:
69             // Can add integer to RawTypedIdentifier to get RawTypedIdentifier
70             ref RawTypedIdentifier opOpAssign(string op)(in T rhs) nothrow @safe @nogc if (op == "+" || op == "-") {
71                 mixin("_value "~op~"= rhs;");
72                 return this;
73             }
74 
75             // Can do any op at all on another TypedId
76             ref RawTypedIdentifier opOpAssign(string op)(in RawTypedIdentifier rhs) nothrow @safe @nogc {
77                 mixin("_value "~op~"= rhs.value;");
78                 return this;
79             }
80 
81             // Non-modifying op with integer
82             RawTypedIdentifier opBinary(string op)(in T rhs) const pure nothrow @safe @nogc if (op == "+" || op == "-") {
83                 RawTypedIdentifier res = this;
84                 // Forward to opOpAssign
85                 mixin("res "~op~"= rhs;");
86 
87                 return res;
88             }
89 
90             ref RawTypedIdentifier opUnary(string op)() nothrow @safe @nogc if (op == "++" || op == "--") {
91                 mixin("_value" ~ op ~ ";");
92 
93                 return this;
94             }
95 
96             // Can subtract two RawTypedIdentifier to get an integer
97             T opBinary(string op : "-")(in RawTypedIdentifier rhs) const pure nothrow @safe @nogc {
98                 return value - rhs.value;
99             }
100         }
101 
102         // toString is not @nogc
103         string toString() const pure nothrow @safe {
104             return name ~ "<" ~ (isValid ? to!string(_value) : "INVALID") ~ ">";
105         }
106 
107         @notrace void setFromString(string str) @safe {
108             import std.conv : ConvException, parse;
109             import std..string : indexOf;
110             auto idx = str.indexOf('<');
111             if (idx > 0 && str[$ - 1] == '>') {
112                 import std.uni: sicmp;
113                 enforceFmt!ConvException(sicmp(str[0 .. idx], name) == 0, "Expected %s not %s", name, str[0 .. idx]);
114                 auto tmp2 = str[idx + 1 .. $-1];
115                 if (tmp2 == "INVALID") {
116                     _value = _invalid;
117                 } else {
118                     _value = parse!T(tmp2);
119                 }
120             }
121             else {
122                 _value = parse!T(str);
123             }
124         }
125 
126         alias Allocator = CustomAllocator!true;
127         static struct CustomAllocator(bool SkipInvalid, T Min = T.min, T Max = T.max) {
128         private:
129             T nextValue = Min;
130 
131         public:
132             this( RawTypedIdentifier initValue ) {
133                 this.nextValue = initValue.value;
134             }
135 
136             @notrace RawTypedIdentifier getNext() nothrow @safe @nogc {
137                 T next = nextValue++;
138 
139                 if( next>Max ) {
140                     nextValue = Min;
141                     return getNext();
142                 }
143 
144                 static if(SkipInvalid) {
145                     if( next==_invalid )
146                         return getNext();
147                 }
148 
149                 return RawTypedIdentifier(next);
150             }
151 
152             void reset(RawTypedIdentifier value = RawTypedIdentifier(Min)) nothrow @safe @nogc {
153                 DBG_ASSERT!"Reset value must be in legal range %s<=%s<=%s"(
154                         value.value>=Min && value.value<=Max, Min, value, Max);
155                 nextValue = value.value;
156             }
157 
158             static assert (Allocator.sizeof == RawTypedIdentifier.sizeof);
159         }
160 
161         static assert (this.sizeof == T.sizeof);
162     }
163 }
164 
165 alias TypedIdentifier(string name, T, T invalid = T.max, T _init = T.init, FMT fmt = FMT("")) =
166     RawTypedIdentifier!(name, T, invalid, _init, fmt, false);
167 alias AlgebraicTypedIdentifier(string name, T, T invalid = T.max, T _init = T.init, FMT fmt = FMT("")) =
168     RawTypedIdentifier!(name, T, invalid, _init, fmt, true);
169 
170 unittest {
171     import std..string;
172 
173     alias UtId = AlgebraicTypedIdentifier!("UtId", ushort);
174 
175     auto val = UtId(12);
176     auto inv = UtId(65535);
177 
178     assert( format("%s", val) == "UtId<12>" );
179     assert( format("%s", inv) == "UtId<INVALID>" );
180 
181     val++;
182     inv++;
183     assert( val == UtId(13) );
184     //assert( !inv.isValid );
185 
186     val += 2;
187     assert( val == UtId(15) );
188 
189     auto newval = val + 3;
190 
191     static assert( is( typeof(newval) == typeof(val) ) );
192     assert( newval.value == 18 );
193 
194     newval = val - 4;
195     assert( newval.value == 11 );
196 }
197 
198 enum isTypedIdentifier(T) = isInstanceOf!(RawTypedIdentifier, T);
199 enum isAlgebraicTypedIdentifier(T) = {
200     static if( isTypedIdentifier!T ) {
201         return T.Algebraic;
202     } else {
203         return false;
204     }
205 } ();
206 
207 auto iota(T, U)(const T start, const T end, const U step) nothrow @safe @nogc
208     if (isAlgebraicTypedIdentifier!T && is(U == T.UnderlyingType))
209 {
210     import std.range : iota;
211     import std.algorithm : map;
212     alias AlgType = PromotedType!U;
213     return iota(AlgType(start.value), AlgType(end.value), AlgType(step)).map!(x => T(cast(U)x));
214 }
215 
216 auto iota(T, U)(const T start, const T end, const U step) nothrow @safe @nogc
217     if(isAlgebraicTypedIdentifier!T && !is(U == T.UnderlyingType) && isImplicitlyConvertible!(U, T.UnderlyingType))
218 {
219     return iota(start, end, T.UnderlyingType(step));
220 }
221 
222 auto iota(T)(const T start, const T end) nothrow @safe @nogc if (isAlgebraicTypedIdentifier!T) {
223     return iota(start, end, T.UnderlyingType(1));
224 }
225 
226 auto iota(T)(const T end) nothrow @safe @nogc if (isAlgebraicTypedIdentifier!T) {
227     return iota(T(0), end);
228 }
229 
230 unittest {
231     alias T = AlgebraicTypedIdentifier!("SomeType", uint);
232     import std.algorithm : equal;
233     static assert(iota(T(12), T(15), ubyte(2)).equal([T(12), T(14)]));
234 }
235 
236 unittest {
237     alias Foo = AlgebraicTypedIdentifier!("FOO", ubyte);
238     bool verify() {
239         import std.algorithm.comparison : equal;
240         assert(iota(Foo(2)).equal([Foo(0), Foo(1)]));
241         assert(iota(Foo(2), Foo(4)).equal([Foo(2), Foo(3)]));
242         assert(iota(Foo(2), Foo(6), ubyte(2)).equal([Foo(2), Foo(4)]));
243         return true;
244     }
245     static assert(verify());
246 }
247 
248 unittest {
249     import std.range : iota;
250 
251     alias UTiD = AlgebraicTypedIdentifier!("UTiD", uint);
252 
253     uint counter;
254     foreach( i; iota(12) ) {
255         counter++;
256     }
257     assert(counter==12);
258 
259     counter = 0;
260     foreach( i; iota(UTiD(12)) ) {
261         counter++;
262     }
263     assert(counter==12);
264 
265     counter = 0;
266     foreach( i; iota(UTiD(3), UTiD(12)) ) {
267         counter++;
268     }
269     assert(counter == 9);
270 }
271 
272 import std.range : isInputRange, ElementType;
273 import std.array : empty, front, popFront;
274 
275 struct TypedIndexArray(KEY, VALUE, size_t LENGTH) if (isAlgebraicTypedIdentifier!KEY) {
276 private:
277     VALUE[LENGTH] _array;
278 
279 public:
280     alias Key = KEY;
281 
282     this(R)(R values) if(isInputRange!R) {
283         // DMDBUG Work around: https://issues.dlang.org/show_bug.cgi?id=16301
284         // Allowing TypedIndexArray to be used in more CTFE contexts:
285         auto arr = &this._array;
286         uint count = 0;
287         foreach(ref item; values) {
288             (*arr)[count] = item;
289             count++;
290         }
291         ASSERT!("TypedIndexArray initialize from range of wrong length (%s != " ~ LENGTH.stringof ~ ")") (LENGTH == count, count);
292     }
293     this()(VALUE value) {
294         _array[] = value;
295     }
296 
297     this()(ref VALUE[LENGTH] value) {
298         _array[] = value[];
299     }
300 
301     alias Slice = TypedIndexSlice!(KEY, VALUE);
302     ref inout(VALUE) opIndex(KEY index) inout {
303         DBG_ASSERT!"opIndex called with invalid index"(index.isValid);
304         return _array[index.value];
305     }
306 
307     inout(Slice) opIndex() inout {
308         return inout(Slice)(_array[]);
309     }
310 
311     inout(Slice) opSlice(KEY begin, KEY end) inout {
312         return inout(Slice)(_array[begin.value .. end.value], begin.value);
313     }
314 
315     @property KEY opDollar() const { return KEY(LENGTH); }
316     @property static KEY length() { return KEY(LENGTH); }
317     static size_t intLength() { return LENGTH; }
318 
319     int opApply(scope int delegate(ref VALUE value) dg) {
320         return this[].opApply(dg);
321     }
322 
323     int opApply(scope int delegate(ref const(VALUE) value) dg) const {
324         return this[].opApply(dg);
325     }
326 
327     int opApply(scope int delegate(KEY key, ref VALUE value) dg) {
328         return this[].opApply(dg);
329     }
330 
331     int opApply(scope int delegate(KEY key, ref const(VALUE) value) dg) const {
332         return this[].opApply(dg);
333     }
334 
335     ref TypedIndexArray opAssign()(VALUE value) @nogc {
336         _array[] = value;
337 
338         return this;
339     }
340 
341     ref TypedIndexArray opAssign()(ref VALUE[LENGTH] value) {
342         _array[] = value[];
343 
344         return this;
345     }
346 
347     ref TypedIndexArray opOpAssign(string OP)(VALUE value) {
348         this[].opOpAssign!OP(value);
349 
350         return this;
351     }
352 
353     ref TypedIndexArray opIndexAssign()(VALUE value, KEY key) {
354         _array[key.value] = value;
355 
356         return this;
357     }
358 
359     ref TypedIndexArray opIndexAssign()(VALUE value) {
360         this[].opIndexAssign(value);
361 
362         return this;
363     }
364 
365     ref TypedIndexArray opIndexAssign()(VALUE value, Slice slice) {
366         slice.opIndexAssign(value);
367 
368         return this;
369     }
370 
371     ref TypedIndexArray opIndexOpAssign(string OP, T)(T value, KEY key) {
372         this[].opIndexOpAssign!OP(value, key);
373 
374         return this;
375     }
376 
377     ref TypedIndexArray opIndexOpAssign(string OP, T)(T value) {
378         this[].opIndexOpAssign!OP(value);
379 
380         return this;
381     }
382 
383     ref TypedIndexArray opIndexOpAssign(string OP, T)(T value, Slice slice) {
384         slice.opIndexOpAssign!OP(value);
385 
386         return this;
387     }
388 
389     @property ref inout(VALUE[LENGTH]) range() inout {
390         return _array;
391     }
392 }
393 
394 auto mapTypedArray(alias _func, KEY, VALUE, size_t LENGTH)(ref TypedIndexArray!(KEY, VALUE, LENGTH) arr) {
395     import std.functional : unaryFun;
396     import std.traits;
397     alias func = unaryFun!_func;
398     alias RetType = typeof(func(arr._array[0]));
399     TypedIndexArray!(KEY, Unqual!RetType, LENGTH) result;
400     foreach(KEY idx, ref VALUE oldVal; arr) {
401         result[idx] = func(oldVal);
402     }
403     return result;
404 }
405 
406 // Similar to std.array : array
407 auto typedIndexArray(KEY, size_t LENGTH, R)(R items) if(isInputRange!R)
408 {
409     alias Array = TypedIndexArray!(KEY, ElementType!R, LENGTH);
410     return Array(items);
411 }
412 
413 unittest {
414     import std.range : iota, take;
415     import std.array : array;
416     int[10] x = iota(10).array;
417     alias Meters = AlgebraicTypedIdentifier!("Meters", uint);
418     auto meters = typedIndexArray!(Meters, 10)(x[]);
419     assert(meters[Meters(0)] == 0);
420     assert(meters[Meters(9)] == 9);
421     assert(meters.length == Meters(10));
422 
423     import std.algorithm : map, equal;
424     assert(meters[].map!(x => x*2).take(5).equal([0, 2, 4, 6, 8]));
425 }
426 
427 struct TypedIndexSlice(KEY, VALUE) if (isAlgebraicTypedIdentifier!KEY) {
428 private:
429     VALUE[] _slice;
430     KEY.UnderlyingType _offset = 0;
431 
432 public:
433     alias Key = KEY;
434 
435     ref inout(VALUE) opIndex(KEY index) inout {
436         DBG_ASSERT!"opIndex called with invalid index"(index.isValid);
437         return _slice[index.value - _offset];
438     }
439 
440     ref inout(TypedIndexSlice) opIndex() inout {
441         return this;
442     }
443 
444     inout(TypedIndexSlice) opSlice(KEY begin, KEY end) inout {
445         return inout(TypedIndexSlice)(_slice[begin.value - _offset .. end.value - _offset], begin.value);
446     }
447 
448     @property KEY opDollar() const { return KEY(cast(KEY.UnderlyingType)(length + _offset)); }
449     @property size_t length() const { return _slice.length; }
450 
451     // DMDBUG: Yes, it is a direct dupliate of the opApply below. Yes, this is precisely what inout was invented
452     // for. No, inout doesn't work here. This is because opApply inference is special-cased and doesn't have the full
453     // delegate type, but rather must select the overloaded method via "ref" or "const" syntax in the foreach and decl.
454     int opApply(scope int delegate(KEY key, ref const(VALUE) value) dg) const {
455         foreach( KEY i; iota(KEY(_offset), opDollar) ) {
456             int result = dg(i, _slice[i.value - _offset]);
457             if (result) return result;
458         }
459         return 0;
460     }
461 
462     int opApply(scope int delegate(KEY key, ref VALUE value) dg) {
463         foreach( KEY i; iota(KEY(_offset), opDollar) ) {
464             int result = dg(i, _slice[i.value - _offset]);
465             if (result) return result;
466         }
467         return 0;
468     }
469 
470     int opApply(scope int delegate(ref const(VALUE) value) dg) const {
471         return opApply((KEY key, ref const(VALUE) val) => dg(val));
472     }
473 
474     int opApply(scope int delegate(ref VALUE value) dg) {
475         return opApply((KEY key, ref VALUE val) => dg(val));
476     }
477 
478     @property bool empty() { return _slice.empty; }
479     @property ref inout(VALUE) front() inout { return _slice.front; }
480     void popFront() { _slice.popFront(); _offset++; }
481 
482     // TODO: Rename to slice
483     @property inout(VALUE)[] range() inout {
484         return _slice;
485     }
486 
487     ref TypedIndexSlice opAssign()(VALUE value) @nogc {
488         _slice[] = value;
489 
490         return this;
491     }
492 
493     ref TypedIndexSlice opOpAssign(string OP)(VALUE value) @nogc {
494         import std.format : format;
495         mixin( q{_slice[] %s= value;}.format(OP) );
496 
497         return this;
498     }
499 
500     ref TypedIndexSlice opIndexAssign()(VALUE value, KEY key) @nogc {
501         _slice[key.value - _offset] = value;
502 
503         return this;
504     }
505 
506     ref TypedIndexSlice opIndexAssign()(VALUE value) @nogc {
507         _slice[] = value;
508 
509         return this;
510     }
511 
512     ref TypedIndexSlice opIndexOpAssign(string OP, T)(T value, KEY key) @nogc {
513         import std.format : format;
514         mixin(q{_slice[key.value - _offset] %s= value;}.format(OP));
515 
516         return this;
517     }
518 
519     ref TypedIndexSlice opIndexOpAssign(string OP, T)(T value) @nogc {
520         import std.format : format;
521         mixin(q{_slice[] %s= value}.format(OP));
522 
523         return this;
524     }
525 }
526 
527 
528 unittest {
529     import std.stdio;
530     import std.conv;
531 
532     alias XXNodeId = AlgebraicTypedIdentifier!("XXNodeId", int);
533     alias XXBucketId = TypedIdentifier!("XXBucketId", ushort, 0);
534     alias MoisheId = TypedIdentifier!("MoisheId", ulong);
535 
536     auto x = XXNodeId(5);
537     auto y = XXBucketId(17);
538     MoisheId z = 19;
539 
540     assert(to!string(x) == "XXNodeId<5>");
541     assert(y.value == 17);
542     // assert(z == 19);
543     static assert (XXNodeId.invalid == XXNodeId(int.max));
544     static assert (XXBucketId.invalid == XXBucketId(0));
545 
546     static assert(!__traits(compiles, x=y));
547 
548     static assert(isTypedIdentifier!XXNodeId);
549     static assert(isTypedIdentifier!XXBucketId);
550     static assert(isTypedIdentifier!MoisheId);
551     static assert(!isTypedIdentifier!int);
552 }