1 module nxt.typecons_ex; 2 3 /+ TODO: Move to Phobos and refer to http://forum.dlang.org/thread/lzyqywovlmdseqgqfvun@forum.dlang.org#post-ibvkvjwexdafpgtsamut:40forum.dlang.org +/ 4 /+ TODO: Better with?: +/ 5 /* inout(Nullable!T) nullable(T)(inout T a) */ 6 /* { */ 7 /* return typeof(return)(a); */ 8 /* } */ 9 /* inout(Nullable!(T, nullValue)) nullable(alias nullValue, T)(inout T value) */ 10 /* if (is (typeof(nullValue) == T)) */ 11 /* { */ 12 /* return typeof(return)(value); */ 13 /* } */ 14 15 import std.typecons : Nullable, NullableRef; 16 17 /** Instantiator for `Nullable`. 18 */ 19 auto nullable(T)(T a) 20 { 21 return Nullable!T(a); 22 } 23 24 /// 25 pure nothrow @safe @nogc unittest { 26 auto x = 42.5.nullable; 27 assert(is(typeof(x) == Nullable!double)); 28 } 29 30 /** Instantiator for `Nullable`. 31 */ 32 auto nullable(alias nullValue, T)(T value) 33 if (is (typeof(nullValue) == T)) 34 { 35 return Nullable!(T, nullValue)(value); 36 } 37 38 /// 39 pure nothrow @safe @nogc unittest { 40 auto x = 3.nullable!(int.max); 41 assert(is (typeof(x) == Nullable!(int, int.max))); 42 } 43 44 /** Instantiator for `NullableRef`. 45 */ 46 auto nullableRef(T)(T* a) @safe pure nothrow 47 { 48 return NullableRef!T(a); 49 } 50 51 /// 52 /*TODO: @safe*/ pure nothrow @nogc unittest { 53 auto x = 42.5; 54 auto xr = nullableRef(&x); 55 assert(!xr.isNull); 56 xr.nullify; 57 assert(xr.isNull); 58 } 59 60 /** See_Also: http://forum.dlang.org/thread/jwdbjlobbilowlnpdzzo@forum.dlang.org 61 */ 62 template New(T) 63 if (is(T == class)) 64 { 65 T New(Args...) (Args args) 66 { 67 return new T(args); 68 } 69 } 70 71 import std.traits : isArray, isUnsigned; 72 import std.range.primitives : hasSlicing; 73 74 /** Check if `T` is castable to `U`. 75 */ 76 enum isCastableTo(T, U) = __traits(compiles, { cast(U)(T.init); }); 77 78 enum isIndex(I) = (is(I == enum) || 79 isUnsigned!I || /+ TODO: should we allow isUnsigned here? +/ 80 isCastableTo!(I, size_t)); 81 82 /** Check if `T` can be indexed by an instance of `I`. 83 * 84 * See_Also: http://forum.dlang.org/post/ajxtksnsxqmeulsedmae@forum.dlang.org 85 * 86 * TODO: move to traits_ex.d 87 * TODO: Move to Phobos 88 */ 89 enum hasIndexing(T, I = size_t) = is(typeof(T.init[I.init]) == typeof(T.init[0])); 90 91 /// 92 pure nothrow @safe @nogc unittest { 93 static assert(!hasIndexing!(int)); 94 static assert(hasIndexing!(int[3])); 95 static assert(hasIndexing!(byte[])); 96 static assert(hasIndexing!(byte[], uint)); 97 static assert(hasIndexing!(string)); 98 } 99 100 /** Check if `R` is indexable by `I`. */ 101 enum isIndexableBy(R, I) = (hasIndexing!R && isIndex!I); 102 103 /// 104 pure nothrow @safe @nogc unittest { 105 static assert(isIndexableBy!(int[3], ubyte)); 106 } 107 108 /** Check if `R` is indexable by a automatically `R`-local defined integer type named `I`. 109 */ 110 enum isIndexableBy(R, alias I) = (hasIndexing!R && is(string == typeof(I))); /+ TODO: extend to isSomeString? +/ 111 112 /// 113 pure nothrow @safe @nogc unittest { 114 static assert(isIndexableBy!(int[], "I")); 115 } 116 117 /** Generate bounds-checked `opIndex` and `opSlice`. 118 */ 119 static private 120 mixin template _genIndexAndSliceOps(I) 121 { 122 import nxt.conv_ex : toDefaulted; 123 124 // indexing 125 126 /// Get element at compile-time index `i`. 127 auto ref at(size_t i)() inout 128 { 129 assert(cast(size_t)i < _r.length, "Index " ~ i ~ " must be smaller than array length" ~ _r.length.stringof); 130 return _r[i]; 131 } 132 133 /// Get element at index `i`. 134 auto ref opIndex(I i) inout 135 { 136 assert(cast(size_t)i < _r.length, "Range violation with index of type " ~ I.stringof); 137 return _r[cast(size_t)i]; 138 } 139 140 /// Set element at index `i` to `value`. 141 auto ref opIndexAssign(V)(V value, I i) 142 { 143 assert(cast(size_t)i < _r.length, "Range violation with index of type " ~ I.stringof); 144 145 import core.lifetime : move; 146 147 move(value, _r[cast(size_t)i]); 148 return _r[cast(size_t)i]; 149 } 150 151 // slicing 152 static if (hasSlicing!R) 153 { 154 auto ref opSlice(I i, I j) inout 155 { 156 return _r[cast(size_t)i .. cast(size_t)j]; 157 } 158 auto ref opSliceAssign(V)(V value, I i, I j) 159 { 160 return _r[cast(size_t)i .. cast(size_t)j] = value; /+ TODO: use `move()` +/ 161 } 162 } 163 } 164 165 /** Generate @trusted non-bounds-checked `opIndex` and `opSlice`. 166 */ 167 static private 168 mixin template _genIndexAndSliceOps_unchecked(I) 169 { 170 @trusted: 171 172 // indexing 173 174 /// Get element at compile-time index `i`. 175 auto ref at(size_t i)() inout 176 { 177 static assert(i < _r.length, "Index " ~ i.stringof ~ " must be smaller than array length " ~ _r.length.stringof); 178 return _r.ptr[i]; 179 } 180 181 /// Get element at index `i`. 182 auto ref opIndex(I i) inout 183 { 184 return _r.ptr[cast(size_t)i]; // safe to avoid range checking 185 } 186 187 /// Set element at index `i` to `value`. 188 auto ref opIndexAssign(V)(V value, I i) 189 { 190 return _r.ptr[cast(size_t)i] = value; 191 } 192 193 // slicing 194 static if (hasSlicing!R) 195 { 196 auto ref opSlice(I i, I j) inout { return _r.ptr[cast(size_t)i .. 197 cast(size_t)j]; } 198 auto ref opSliceAssign(V)(V value, I i, I j) { return _r.ptr[cast(size_t)i .. 199 cast(size_t)j] = value; } 200 } 201 } 202 203 /** Wrapper for `R` with Type-Safe `I`-Indexing. 204 See_Also: http://forum.dlang.org/thread/gayfjaslyairnzrygbvh@forum.dlang.org#post-gayfjaslyairnzrygbvh:40forum.dlang.org 205 206 TODO: Merge with https://github.com/rcorre/enumap 207 208 TODO: Use std.range.indexed when I is an enum with non-contigious 209 enumerators. Perhaps use among aswell. 210 211 TODO: Rename to something more concise such as [Bb]y. 212 213 TODO: Allow `I` to be a string and if so derive `Index` to be that string. 214 */ 215 struct IndexedBy(R, I) 216 if (isIndexableBy!(R, I)) 217 { 218 alias Index = I; /// indexing type 219 mixin _genIndexAndSliceOps!I; 220 R _r; 221 alias _r this; /+ TODO: Use opDispatch instead; to override only opSlice and opIndex +/ 222 } 223 224 /** Statically-Sized Array of ElementType `E` indexed by `I`. 225 TODO: assert that `I` is continuous if it is a `enum`. 226 */ 227 struct IndexedArray(E, I) 228 if (isIndex!I) 229 { 230 static assert(I.min == 0, "Index type I is currently limited to start at 0 and be continuous"); 231 alias Index = I; /// indexing type 232 mixin _genIndexAndSliceOps!I; 233 alias R = E[I.max + 1]; // needed by mixins 234 R _r; // static array 235 alias _r this; /+ TODO: Use opDispatch instead; to override only opSlice and opIndex +/ 236 } 237 238 /// 239 pure nothrow @safe unittest { 240 enum N = 7; 241 enum I { x = 0, y = 1, z = 2} 242 alias E = int; 243 alias A = IndexedArray!(E, I); 244 static assert(A.sizeof == 3*int.sizeof); 245 A x; 246 x[I.x] = 1; 247 x[I.y] = 2; 248 x[I.z] = 3; 249 static assert(!__traits(compiles, { x[1] = 3; })); // no integer indexing 250 } 251 252 /** Instantiator for `IndexedBy`. 253 */ 254 auto indexedBy(I, R)(R range) 255 if (isIndexableBy!(R, I)) 256 { 257 return IndexedBy!(R, I)(range); 258 } 259 260 struct IndexedBy(R, string IndexTypeName) 261 if (hasIndexing!R && 262 IndexTypeName != "IndexTypeName") // prevent name lookup failure 263 { 264 static if (__traits(isStaticArray, R)) // if length is known at compile-time 265 { 266 import nxt.modulo : Mod; 267 mixin(`alias ` ~ IndexTypeName ~ ` = Mod!(R.length);`); /+ TODO: relax integer precision argument of `Mod` +/ 268 269 // dummy variable needed for symbol argument to `_genIndexAndSliceOps_unchecked` 270 mixin(`private static alias I__ = ` ~ IndexTypeName ~ `;`); 271 272 mixin _genIndexAndSliceOps_unchecked!(I__); // no range checking needed because I is always < R.length 273 274 /** Get index of element `E` wrapped in a `bool`-convertable struct. */ 275 auto findIndex(E)(E e) pure nothrow @safe @nogc 276 { 277 static struct Result 278 { 279 Index index; // index if exists is `true', 0 otherwise 280 bool exists; // `true` iff `index` is defined, `false` otherwise 281 bool opCast(T : bool)() const pure nothrow @safe @nogc { return exists; } 282 } 283 import std.algorithm : countUntil; 284 const ix = _r[].countUntil(e); // is safe 285 if (ix >= 0) 286 { 287 return Result(Index(ix), true); 288 } 289 return Result(Index(0), false); 290 } 291 } 292 else 293 { 294 mixin(q{ struct } ~ IndexTypeName ~ 295 q{ { 296 alias T = size_t; 297 this(T ix) { this._ix = ix; } 298 T opCast(U : T)() const { return _ix; } 299 private T _ix = 0; 300 } 301 }); 302 mixin _genIndexAndSliceOps!(mixin(IndexTypeName)); 303 } 304 R _r; 305 alias _r this; /+ TODO: Use opDispatch instead; to override only opSlice and opIndex +/ 306 } 307 308 /* Wrapper type for `R' indexable/sliceable only with type `R.Index`. */ 309 template TypesafelyIndexed(R) 310 if (hasIndexing!R) // prevent name lookup failure 311 { 312 alias TypesafelyIndexed = IndexedBy!(R, "Index"); 313 } 314 315 /** Instantiator for `IndexedBy`. 316 */ 317 auto indexedBy(string I, R)(R range) 318 if (isArray!R && 319 I != "IndexTypeName") // prevent name lookup failure 320 { 321 return IndexedBy!(R, I)(range); 322 } 323 324 /** Instantiator for `TypesafelyIndexed`. 325 */ 326 auto strictlyIndexed(R)(R range) 327 if (hasIndexing!(R)) 328 { 329 return TypesafelyIndexed!(R)(range); 330 } 331 332 /// 333 pure nothrow @safe unittest { 334 enum m = 3; 335 int[m] x = [11, 22, 33]; 336 auto y = x.strictlyIndexed; 337 338 alias Y = typeof(y); 339 340 static assert(is(typeof(y.findIndex(11).index) == Y.Index)); 341 342 assert(y.findIndex(11).exists); 343 assert(y.findIndex(22).exists); 344 assert(y.findIndex(33).exists); 345 346 if (auto hit = y.findIndex(11)) { assert(hit.index == 0); } else { assert(false); } 347 if (auto hit = y.findIndex(22)) { assert(hit.index == 1); } else { assert(false); } 348 if (auto hit = y.findIndex(33)) { assert(hit.index == 2); } else { assert(false); } 349 350 assert(!y.findIndex(44)); 351 assert(!y.findIndex(55)); 352 353 assert(!y.findIndex(44).exists); 354 assert(!y.findIndex(55).exists); 355 } 356 357 /// 358 pure nothrow @safe unittest { 359 enum N = 7; 360 alias T = TypesafelyIndexed!(size_t[N]); // static array 361 static assert(T.sizeof == N*size_t.sizeof); 362 import nxt.modulo : Mod, mod; 363 364 T x; 365 366 x[Mod!N(1)] = 11; 367 x[1.mod!N] = 11; 368 assert(x[1.mod!N] == 11); 369 370 x.at!1 = 12; 371 static assert(!__traits(compiles, { x.at!N; })); 372 assert(x.at!1 == 12); 373 } 374 375 /// 376 pure nothrow @safe unittest { 377 int[3] x = [1, 2, 3]; 378 379 // sample index 380 struct Index(T = size_t) 381 if (isUnsigned!T) 382 { 383 this(T i) { this._i = i; } 384 T opCast(U : T)() const { return _i; } 385 private T _i = 0; 386 } 387 alias J = Index!size_t; 388 389 enum E { e0, e1, e2 } 390 391 with (E) 392 { 393 auto xb = x.indexedBy!ubyte; 394 auto xi = x.indexedBy!uint; 395 auto xj = x.indexedBy!J; 396 auto xe = x.indexedBy!E; 397 auto xf = x.strictlyIndexed; 398 399 auto xs = x.indexedBy!"I"; 400 alias XS = typeof(xs); 401 XS xs_; 402 403 // indexing with correct type 404 xb[ 0 ] = 11; assert(xb[ 0 ] == 11); 405 xi[ 0 ] = 11; assert(xi[ 0 ] == 11); 406 xj[J(0)] = 11; assert(xj[J(0)] == 11); 407 xe[ e0 ] = 11; assert(xe[ e0 ] == 11); 408 409 // indexing with wrong type 410 static assert(!__traits(compiles, { xb[J(0)] = 11; })); 411 static assert(!__traits(compiles, { xi[J(0)] = 11; })); 412 static assert(!__traits(compiles, { xj[ 0 ] = 11; })); 413 static assert(!__traits(compiles, { xe[ 0 ] = 11; })); 414 static assert(!__traits(compiles, { xs[ 0 ] = 11; })); 415 static assert(!__traits(compiles, { xs_[ 0 ] = 11; })); 416 417 import std.algorithm.comparison : equal; 418 import std.algorithm.iteration : filter; 419 420 assert(equal(xb[].filter!(a => a < 11), [2, 3])); 421 assert(equal(xi[].filter!(a => a < 11), [2, 3])); 422 assert(equal(xj[].filter!(a => a < 11), [2, 3])); 423 assert(equal(xe[].filter!(a => a < 11), [2, 3])); 424 // assert(equal(xs[].filter!(a => a < 11), [2, 3])); 425 } 426 } 427 428 /// 429 pure nothrow @safe unittest { 430 auto x = [1, 2, 3]; 431 432 // sample index 433 struct Index(T = size_t) 434 if (isUnsigned!T) 435 { 436 this(T ix) { this._ix = ix; } 437 T opCast(U : T)() const { return _ix; } 438 private T _ix = 0; 439 } 440 alias J = Index!size_t; 441 442 enum E { e0, e1, e2 } 443 444 with (E) 445 { 446 auto xb = x.indexedBy!ubyte; 447 auto xi = x.indexedBy!uint; 448 auto xj = x.indexedBy!J; 449 auto xe = x.indexedBy!E; 450 451 // indexing with correct type 452 xb[ 0 ] = 11; assert(xb[ 0 ] == 11); 453 xi[ 0 ] = 11; assert(xi[ 0 ] == 11); 454 xj[J(0)] = 11; assert(xj[J(0)] == 11); 455 xe[ e0 ] = 11; assert(xe[ e0 ] == 11); 456 457 // slicing with correct type 458 xb[ 0 .. 1 ] = 12; assert(xb[ 0 .. 1 ] == [12]); 459 xi[ 0 .. 1 ] = 12; assert(xi[ 0 .. 1 ] == [12]); 460 xj[J(0) .. J(1)] = 12; assert(xj[J(0) .. J(1)] == [12]); 461 xe[ e0 .. e1 ] = 12; assert(xe[ e0 .. e1 ] == [12]); 462 463 // indexing with wrong type 464 static assert(!__traits(compiles, { xb[J(0)] = 11; })); 465 static assert(!__traits(compiles, { xi[J(0)] = 11; })); 466 static assert(!__traits(compiles, { xj[ 0 ] = 11; })); 467 static assert(!__traits(compiles, { xe[ 0 ] = 11; })); 468 469 // slicing with wrong type 470 static assert(!__traits(compiles, { xb[J(0) .. J(0)] = 11; })); 471 static assert(!__traits(compiles, { xi[J(0) .. J(0)] = 11; })); 472 static assert(!__traits(compiles, { xj[ 0 .. 0 ] = 11; })); 473 static assert(!__traits(compiles, { xe[ 0 .. 0 ] = 11; })); 474 475 import std.algorithm.comparison : equal; 476 import std.algorithm.iteration : filter; 477 478 assert(equal(xb.filter!(a => a < 11), [2, 3])); 479 assert(equal(xi.filter!(a => a < 11), [2, 3])); 480 assert(equal(xj.filter!(a => a < 11), [2, 3])); 481 assert(equal(xe.filter!(a => a < 11), [2, 3])); 482 } 483 } 484 485 /// 486 pure nothrow @safe unittest { 487 auto x = [1, 2, 3]; 488 struct I(T = size_t) 489 { 490 this(T ix) { this._ix = ix; } 491 T opCast(U : T)() const { return _ix; } 492 private T _ix = 0; 493 } 494 alias J = I!size_t; 495 auto xj = x.indexedBy!J; 496 } 497 498 /// 499 pure nothrow @safe unittest { 500 auto x = [1, 2, 3]; 501 struct I(T = size_t) 502 { 503 private T _ix = 0; 504 } 505 alias J = I!size_t; 506 static assert(!__traits(compiles, { auto xj = x.indexedBy!J; })); 507 } 508 509 /// 510 version (none) 511 pure nothrow @safe unittest { 512 auto x = [1, 2, 3]; 513 import nxt.bound : Bound; 514 alias B = Bound!(ubyte, 0, 2); 515 B b; 516 auto c = cast(size_t)b; 517 auto y = x.indexedBy!B; 518 } 519 520 /// 521 pure nothrow @safe unittest { 522 import nxt.container.dynamic_array : Array = DynamicArray; 523 524 enum Lang { en, sv, fr } 525 526 alias Ixs = Array!int; 527 528 struct S { 529 Lang lang; 530 string data; 531 Ixs ixs; 532 } 533 534 alias A = Array!S; 535 536 struct I { 537 size_t opCast(U : size_t)() const pure nothrow @safe @nogc { return _ix; } 538 uint _ix; 539 alias _ix this; 540 } 541 542 I i; 543 static assert(isCastableTo!(I, size_t)); 544 static assert(isIndexableBy!(A, I)); 545 546 alias IA = IndexedBy!(A, I); 547 IA ia; 548 ia ~= S.init; 549 assert(ia.length == 1); 550 Ixs ixs; 551 ixs.length = 42; 552 import std.algorithm.mutation : move; 553 auto s = S(Lang.en, "alpha", move(ixs)); /+ TODO: use generic `makeOfLength` +/ 554 555 import core.lifetime : move; 556 557 ia ~= move(s); 558 assert(ia.length == 2); 559 } 560 561 /** Returns: a `string` containing the definition of an `enum` named `name` and 562 with enumerator names given by `Es`, optionally prepended with `prefix` and 563 appended with `suffix`. 564 565 TODO: Move to Phobos std.typecons 566 */ 567 template makeEnumFromSymbolNames(string prefix = `__`, 568 string suffix = ``, 569 bool firstUndefined = true, 570 bool useMangleOf = false, 571 Es...) 572 if (Es.length != 0) 573 { 574 enum members = 575 { 576 string s = firstUndefined ? `undefined, ` : ``; 577 foreach (E; Es) 578 { 579 static if (useMangleOf) 580 { 581 enum E_ = E.mangleof; 582 } 583 else 584 { 585 import std.traits : isPointer; 586 static if (isPointer!E) 587 { 588 import std.traits : TemplateOf; 589 enum isTemplateInstance = is(typeof(TemplateOf!(typeof(*E.init)))); 590 static if (isTemplateInstance) // strip template params for now 591 { 592 enum E_ = __traits(identifier, TemplateOf!(typeof(*E))) ~ `Ptr`; 593 } 594 else 595 { 596 enum E_ = typeof(*E.init).stringof ~ `Ptr`; 597 } 598 } 599 else 600 { 601 enum E_ = E.stringof; 602 } 603 604 } 605 s ~= prefix ~ E_ ~ suffix ~ `, `; 606 } 607 return s; 608 }(); 609 mixin("enum makeEnumFromSymbolNames : ubyte {" ~ members ~ "}"); 610 } 611 612 /// 613 pure nothrow @safe @nogc unittest { 614 import std.meta : AliasSeq; 615 struct E(T) { T x; } 616 alias Types = AliasSeq!(byte, short, int*, E!int*); 617 alias Type = makeEnumFromSymbolNames!(`_`, `_`, true, false, Types); 618 static assert(is(Type == enum)); 619 static assert(Type.undefined.stringof == `undefined`); 620 static assert(Type._byte_.stringof == `_byte_`); 621 static assert(Type._short_.stringof == `_short_`); 622 static assert(Type._intPtr_.stringof == `_intPtr_`); 623 static assert(Type._EPtr_.stringof == `_EPtr_`); 624 } 625 626 /// 627 pure nothrow @safe @nogc unittest { 628 import std.meta : AliasSeq; 629 630 struct E(T) { T x; } 631 632 alias Types = AliasSeq!(byte, short, int*, E!int*); 633 alias Type = makeEnumFromSymbolNames!(`_`, `_`, true, true, Types); 634 635 static assert(is(Type == enum)); 636 637 static assert(Type.undefined.stringof == `undefined`); 638 static assert(Type._g_.stringof == `_g_`); 639 static assert(Type._s_.stringof == `_s_`); 640 static assert(Type._Pi_.stringof == `_Pi_`); 641 } 642 643 /** 644 See_Also: https://p0nce.github.io/d-idioms/#Rvalue-references:-Understanding-auto-ref-and-then-not-using-it 645 */ 646 mixin template RvalueRef() 647 { 648 alias T = typeof(this); // typeof(this) get us the type we're in 649 static assert (is(T == struct)); 650 651 @nogc @safe 652 ref inout(T) asRef() inout pure nothrow return 653 { 654 return this; 655 } 656 } 657 658 @safe @nogc pure nothrow unittest { 659 static struct Vec 660 { 661 @safe @nogc pure nothrow: 662 float x, y; 663 this(float x, float y) pure nothrow 664 { 665 this.x = x; 666 this.y = y; 667 } 668 mixin RvalueRef; 669 } 670 671 static void foo(ref const Vec pos) 672 { 673 } 674 675 Vec v = Vec(42, 23); 676 foo(v); // works 677 foo(Vec(42, 23).asRef); // works as well, and use the same function 678 } 679 680 /** Convert enum value `v` to `string`. 681 See_Also: http://forum.dlang.org/post/aqqhlbaepoimpopvouwv@forum.dlang.org 682 */ 683 string enumToString(E)(E v) 684 { 685 static assert(is(E == enum), "emumToString is only meant for enums"); 686 switch(v) 687 { 688 foreach (m; __traits(allMembers, E)) 689 { 690 case mixin("E." ~ m) : return m; 691 } 692 default: 693 { 694 string result = "cast(" ~ E.stringof ~ ")"; 695 uint val = v; 696 697 enum headLength = E.stringof.length + "cast()".length; 698 uint log10Val = (val < 10) ? 0 : (val < 100) ? 1 : (val < 1_000) ? 2 : 699 (val < 10_000) ? 3 : (val < 100_000) ? 4 : (val < 1_000_000) ? 5 : 700 (val < 10_000_000) ? 6 : (val < 100_000_000) ? 7 : (val < 1000_000_000) ? 8 : 9; 701 702 result.length += log10Val + 1; 703 704 foreach (uint i; 0 .. log10Val + 1) 705 { 706 cast(char)result[headLength + log10Val - i] = cast(char) ('0' + (val % 10)); 707 val /= 10; 708 } 709 return cast(string) result; 710 } 711 } 712 } 713 714 pure nothrow @safe unittest { 715 enum ET { one, two } 716 // static assert(to!string(ET.one) == "one"); 717 static assert (enumToString(ET.one) == "one"); 718 assert (enumToString(ET.one) == "one"); 719 }