1 /** Statically allocated arrays with compile-time known lengths. 2 */ 3 module nxt.fixed_array; 4 5 @safe pure: 6 7 /** Statically allocated `T`-array of fixed pre-allocated length. 8 * 9 * Similar to Rust's `fixedvec`: https://docs.rs/fixedvec/0.2.3/fixedvec/ 10 * Similar to `mir.small_array` at http://mir-algorithm.libmir.org/mir_small_array.html. 11 * 12 * TODO Merge member functions with basic_*_array.d and array_ex.d 13 * 14 * TODO Add @safe nothrow @nogc ctor from static array (of known length) 15 * 16 * TODO use opPostMove (https://github.com/dlang/DIPs/blob/master/DIPs/accepted/DIP1014.md) 17 */ 18 struct FixedArray(T, uint capacity_, bool borrowChecked = false) 19 { 20 // pragma(msg, "T:", T, " capacity_:", capacity_, " borrowChecked:", borrowChecked); 21 import core.exception : onRangeError; 22 import core.lifetime : move, moveEmplace; 23 import std.bitmanip : bitfields; 24 import std.traits : isSomeChar, isAssignable; 25 import core.internal.traits : hasElaborateDestructor; 26 import nxt.container_traits : isAddress; 27 28 alias capacity = capacity_; // for public use 29 30 /// Store of `capacity` number of elements. 31 T[capacity] _store; // TODO use store constructor 32 33 static if (borrowChecked) 34 { 35 /// Number of bits needed to store number of read borrows. 36 private enum readBorrowCountBits = 3; 37 38 /// Maximum value possible for `_readBorrowCount`. 39 enum readBorrowCountMax = 2^^readBorrowCountBits - 1; 40 41 static if (capacity <= 2^^(8*ubyte.sizeof - 1 - readBorrowCountBits) - 1) 42 { 43 private enum lengthMax = 2^^4 - 1; 44 alias Length = ubyte; 45 // TODO make private: 46 mixin(bitfields!(Length, "_length", 4, /// number of defined elements in `_store` 47 bool, "_writeBorrowed", 1, 48 uint, "_readBorrowCount", readBorrowCountBits, 49 )); 50 } 51 else static if (capacity <= 2^^(8*ushort.sizeof - 1 - readBorrowCountBits) - 1) 52 { 53 alias Length = ushort; 54 private enum lengthMax = 2^^14 - 1; 55 // TODO make private: 56 mixin(bitfields!(Length, "_length", 14, /// number of defined elements in `_store` 57 bool, "_writeBorrowed", 1, 58 uint, "_readBorrowCount", readBorrowCountBits, 59 )); 60 } 61 else 62 { 63 static assert("Too large requested capacity " ~ capacity); 64 } 65 } 66 else 67 { 68 static if (capacity <= ubyte.max) 69 { 70 static if (T.sizeof == 1) 71 alias Length = ubyte; // pack length 72 else 73 alias Length = uint; 74 } 75 else static if (capacity <= ushort.max) 76 { 77 static if (T.sizeof <= 2) 78 alias Length = uint; // pack length 79 else 80 alias Length = uint; 81 } 82 else 83 { 84 static assert("Too large requested capacity " ~ capacity); 85 } 86 Length _length; /// number of defined elements in `_store` 87 } 88 89 /// Is `true` iff `U` can be assign to the element type `T` of `this`. 90 private enum isElementAssignable(U) = isAssignable!(T, U); 91 92 /// Construct from element `values`. 93 this(Us...)(Us values) @trusted 94 if (Us.length <= capacity) 95 { 96 foreach (immutable ix, ref value; values) 97 { 98 import nxt.container_traits : needsMove; 99 static if (needsMove!(typeof(value))) 100 { 101 moveEmplace(value, _store[ix]); 102 } 103 else 104 { 105 _store[ix] = value; 106 } 107 } 108 _length = cast(Length)values.length; 109 static if (borrowChecked) 110 { 111 _writeBorrowed = false; 112 _readBorrowCount = 0; 113 } 114 } 115 116 /// Construct from element `values`. 117 this(U)(U[] values) @trusted 118 if (__traits(isCopyable, U)// && 119 // TODO isElementAssignable!U 120 ) // prevent accidental move of l-value `values` in array calls 121 { 122 version(assert) if (values.length > capacity) onRangeError(); // `Arguments don't fit in array` 123 _store[0 .. values.length] = values; 124 _length = cast(Length)values.length; 125 static if (borrowChecked) 126 { 127 _writeBorrowed = false; 128 _readBorrowCount = 0; 129 } 130 } 131 132 /// Construct from element `values`. 133 static typeof(this) fromValuesUnsafe(U)(U[] values) @system 134 if (__traits(isCopyable, U) && 135 isElementAssignable!U 136 ) // prevent accidental move of l-value `values` in array calls 137 { 138 typeof(return) that; // TODO use Store constructor: 139 140 that._store[0 .. values.length] = values; 141 that._length = cast(Length)values.length; 142 143 static if (borrowChecked) 144 { 145 that._writeBorrowed = false; 146 that._readBorrowCount = 0; 147 } 148 149 return that; 150 } 151 152 static if (borrowChecked || 153 hasElaborateDestructor!T) 154 { 155 /** Destruct. */ 156 ~this() @trusted @nogc 157 { 158 static if (borrowChecked) { assert(!isBorrowed); } 159 static if (hasElaborateDestructor!T) 160 { 161 foreach (immutable i; 0 .. length) 162 { 163 .destroy(_store.ptr[i]); 164 } 165 } 166 } 167 } 168 169 /** Add elements `es` to the back. 170 * Throws when array becomes full. 171 * NOTE doesn't invalidate any borrow 172 */ 173 void insertBack(Es...)(Es es) @trusted 174 if (Es.length <= capacity) // TODO use `isAssignable` 175 { 176 version(assert) if (_length + Es.length > capacity) onRangeError(); // `Arguments don't fit in array` 177 foreach (immutable i, ref e; es) 178 { 179 moveEmplace(e, _store[_length + i]); // TODO remove `move` when compiler does it for us 180 } 181 _length = cast(Length)(_length + Es.length); // TODO better? 182 } 183 /// ditto 184 alias put = insertBack; // `OutputRange` support 185 186 /** Try to add elements `es` to the back. 187 * NOTE doesn't invalidate any borrow 188 * Returns: `true` iff all `es` were pushed, `false` otherwise. 189 */ 190 bool insertBackMaybe(Es...)(Es es) @trusted 191 if (Es.length <= capacity) 192 { 193 if (_length + Es.length > capacity) { return false; } 194 foreach (immutable i, ref e; es) 195 { 196 moveEmplace(e, _store[_length + i]); // TODO remove `move` when compiler does it for us 197 } 198 _length = cast(Length)(_length + Es.length); // TODO better? 199 return true; 200 } 201 /// ditto 202 alias putMaybe = insertBackMaybe; 203 204 /** Add elements `es` to the back. 205 * NOTE doesn't invalidate any borrow 206 */ 207 void opOpAssign(string op, Us...)(Us values) 208 if (op == "~" && 209 values.length >= 1 && 210 allSatisfy!(isElementAssignable, Us)) 211 { 212 insertBack(values.move()); // TODO remove `move` when compiler does it for 213 } 214 215 import std.traits : isMutable; 216 static if (isMutable!T) 217 { 218 /** Pop first (front) element. */ 219 auto ref popFront() 220 { 221 assert(!empty); 222 static if (borrowChecked) { assert(!isBorrowed); } 223 // TODO is there a reusable Phobos function for this? 224 foreach (immutable i; 0 .. _length - 1) 225 { 226 move(_store[i + 1], _store[i]); // like `_store[i] = _store[i + 1];` but more generic 227 } 228 _length = cast(typeof(_length))(_length - 1); // TODO better? 229 return this; 230 } 231 } 232 233 /** Pop last (back) element. */ 234 void popBack()() // template-lazy 235 { 236 assert(!empty); 237 static if (borrowChecked) { assert(!isBorrowed); } 238 _length = cast(Length)(_length - 1); // TODO better? 239 static if (hasElaborateDestructor!T) 240 { 241 .destroy(_store.ptr[_length]); 242 } 243 else static if (isAddress!T) 244 { 245 _store.ptr[_length] = null; // please the GC 246 } 247 } 248 249 /** Pop the `n` last (back) elements. */ 250 void popBackN()(size_t n) // template-lazy 251 { 252 assert(length >= n); 253 static if (borrowChecked) { assert(!isBorrowed); } 254 _length = cast(Length)(_length - n); // TODO better? 255 static if (hasElaborateDestructor!T) 256 { 257 foreach (i; 0 .. n) 258 { 259 .destroy(_store.ptr[_length + i]); 260 } 261 } 262 else static if (isAddress!T) // please the GC 263 { 264 foreach (const i; 0 .. n) 265 { 266 _store.ptr[_length + i] = null; 267 } 268 } 269 } 270 271 /** Move element at `index` to return. */ 272 static if (isMutable!T) 273 { 274 /** Pop element at `index`. */ 275 void popAt()(size_t index) // template-lazy 276 @trusted 277 @("complexity", "O(length)") 278 { 279 assert(index < this.length); 280 .destroy(_store.ptr[index]); 281 shiftToFrontAt(index); 282 _length = cast(Length)(_length - 1); 283 } 284 285 T moveAt()(size_t index) // template-lazy 286 @trusted 287 @("complexity", "O(length)") 288 { 289 assert(index < this.length); 290 auto value = _store.ptr[index].move(); 291 shiftToFrontAt(index); 292 _length = cast(Length)(_length - 1); 293 return value; 294 } 295 296 private void shiftToFrontAt()(size_t index) // template-lazy 297 @trusted 298 { 299 foreach (immutable i; 0 .. this.length - (index + 1)) 300 { 301 immutable si = index + i + 1; // source index 302 immutable ti = index + i; // target index 303 moveEmplace(_store.ptr[si], 304 _store.ptr[ti]); 305 } 306 } 307 } 308 309 pragma(inline, true): 310 311 /** Index operator. */ 312 ref inout(T) opIndex(size_t i) inout @trusted return 313 { 314 assert(i < _length); 315 return _store.ptr[i]; 316 } 317 318 /** First (front) element. */ 319 ref inout(T) front() inout @trusted return 320 { 321 assert(!empty); 322 return _store.ptr[0]; 323 } 324 325 /** Last (back) element. */ 326 ref inout(T) back() inout @trusted return 327 { 328 assert(!empty); 329 return _store.ptr[_length - 1]; 330 } 331 332 static if (borrowChecked) 333 { 334 import nxt.borrowed : ReadBorrowed, WriteBorrowed; 335 336 /// Get read-only slice in range `i` .. `j`. 337 auto opSlice(size_t i, size_t j) const return scope { return sliceRO(i, j); } 338 /// Get read-write slice in range `i` .. `j`. 339 auto opSlice(size_t i, size_t j) return scope { return sliceRW(i, j); } 340 341 /// Get read-only full slice. 342 auto opSlice() const return scope { return sliceRO(); } 343 /// Get read-write full slice. 344 auto opSlice() return scope { return sliceRW(); } 345 346 /// Get full read-only slice. 347 ReadBorrowed!(T[], typeof(this)) sliceRO() const @trusted return scope 348 { 349 import core.internal.traits : Unqual; 350 assert(!_writeBorrowed, "Already write-borrowed"); 351 return typeof(return)(_store.ptr[0 .. _length], 352 cast(Unqual!(typeof(this))*)(&this)); // trusted unconst casta 353 } 354 355 /// Get read-only slice in range `i` .. `j`. 356 ReadBorrowed!(T[], typeof(this)) sliceRO(size_t i, size_t j) const @trusted return scope 357 { 358 import core.internal.traits : Unqual; 359 assert(!_writeBorrowed, "Already write-borrowed"); 360 return typeof(return)(_store.ptr[i .. j], 361 cast(Unqual!(typeof(this))*)(&this)); // trusted unconst cast 362 } 363 364 /// Get full read-write slice. 365 WriteBorrowed!(T[], typeof(this)) sliceRW() @trusted return scope 366 { 367 assert(!_writeBorrowed, "Already write-borrowed"); 368 assert(_readBorrowCount == 0, "Already read-borrowed"); 369 return typeof(return)(_store.ptr[0 .. _length], &this); 370 } 371 372 /// Get read-write slice in range `i` .. `j`. 373 WriteBorrowed!(T[], typeof(this)) sliceRW(size_t i, size_t j) @trusted return scope 374 { 375 assert(!_writeBorrowed, "Already write-borrowed"); 376 assert(_readBorrowCount == 0, "Already read-borrowed"); 377 return typeof(return)(_store.ptr[0 .. j], &this); 378 } 379 380 @property 381 { 382 /// Returns: `true` iff `this` is either write or read borrowed. 383 bool isBorrowed() const { return _writeBorrowed || _readBorrowCount >= 1; } 384 385 /// Returns: `true` iff `this` is write borrowed. 386 bool isWriteBorrowed() const { return _writeBorrowed; } 387 388 /// Returns: number of read-only borrowers of `this`. 389 uint readBorrowCount() const { return _readBorrowCount; } 390 } 391 } 392 else 393 { 394 /// Get slice in range `i` .. `j`. 395 inout(T)[] opSlice(size_t i, size_t j) @trusted inout return scope 396 { 397 // assert(i <= j); 398 // assert(j <= _length); 399 return _store[i .. j]; // TODO make .ptr work 400 } 401 402 /// Get full slice. 403 inout(T)[] opSlice() @trusted inout return scope 404 { 405 return _store[0 .. _length]; // TODO make .ptr work 406 } 407 } 408 409 @property 410 { 411 /** Returns: `true` iff `this` is empty, `false` otherwise. */ 412 bool empty() const { return _length == 0; } 413 414 /** Returns: `true` iff `this` is full, `false` otherwise. */ 415 bool full() const { return _length == capacity; } 416 417 /** Get length. */ 418 auto length() const { return _length; } 419 alias opDollar = length; /// ditto 420 421 static if (isSomeChar!T) 422 { 423 /** Get as `string`. */ 424 scope const(T)[] toString() const return 425 { 426 return opSlice(); 427 } 428 } 429 } 430 431 /** Comparison for equality. */ 432 bool opEquals()(const scope auto ref typeof(this) rhs) const 433 { 434 return this[] == rhs[]; 435 } 436 /// ditto 437 bool opEquals(U)(const scope U[] rhs) const 438 if (is(typeof(T[].init == U[].init))) 439 { 440 return this[] == rhs; 441 } 442 } 443 444 /** Stack-allocated string of maximum length of `capacity.` 445 * 446 * Similar to `mir.small_string` at http://mir-algorithm.libmir.org/mir_small_string.html. 447 */ 448 alias StringN(uint capacity, bool borrowChecked = false) = FixedArray!(immutable(char), capacity, borrowChecked); 449 450 /** Stack-allocated wstring of maximum length of `capacity.` */ 451 alias WStringN(uint capacity, bool borrowChecked = false) = FixedArray!(immutable(wchar), capacity, borrowChecked); 452 453 /** Stack-allocated dstring of maximum length of `capacity.` */ 454 alias DStringN(uint capacity, bool borrowChecked = false) = FixedArray!(immutable(dchar), capacity, borrowChecked); 455 456 /** Stack-allocated mutable string of maximum length of `capacity.` */ 457 alias MutableStringN(uint capacity, bool borrowChecked = false) = FixedArray!(char, capacity, borrowChecked); 458 459 /** Stack-allocated mutable wstring of maximum length of `capacity.` */ 460 alias MutableWStringN(uint capacity, bool borrowChecked = false) = FixedArray!(char, capacity, borrowChecked); 461 462 /** Stack-allocated mutable dstring of maximum length of `capacity.` */ 463 alias MutableDStringN(uint capacity, bool borrowChecked = false) = FixedArray!(char, capacity, borrowChecked); 464 465 /// construct from array may throw 466 @safe pure unittest 467 { 468 enum capacity = 3; 469 alias T = int; 470 alias A = FixedArray!(T, capacity); 471 static assert(!mustAddGCRange!A); 472 473 auto a = A([1, 2, 3].s[]); 474 assert(a[] == [1, 2, 3].s); 475 } 476 477 /// unsafe construct from array 478 @trusted pure nothrow @nogc unittest 479 { 480 enum capacity = 3; 481 alias T = int; 482 alias A = FixedArray!(T, capacity); 483 static assert(!mustAddGCRange!A); 484 485 auto a = A.fromValuesUnsafe([1, 2, 3].s); 486 assert(a[] == [1, 2, 3].s); 487 } 488 489 /// construct from scalars is nothrow 490 @safe pure nothrow @nogc unittest 491 { 492 enum capacity = 3; 493 alias T = int; 494 alias A = FixedArray!(T, capacity); 495 static assert(!mustAddGCRange!A); 496 497 auto a = A(1, 2, 3); 498 assert(a[] == [1, 2, 3].s); 499 500 static assert(!__traits(compiles, { auto _ = A(1, 2, 3, 4); })); 501 } 502 503 /// scope checked string 504 @safe pure unittest 505 { 506 enum capacity = 15; 507 foreach (StrN; AliasSeq!(StringN// , WStringN, DStringN 508 )) 509 { 510 alias String15 = StrN!(capacity); 511 512 typeof(String15.init[0])[] xs; 513 auto x = String15("alphas"); 514 515 assert(x[0] == 'a'); 516 assert(x[$ - 1] == 's'); 517 518 assert(x[0 .. 2] == "al"); 519 assert(x[] == "alphas"); 520 521 const y = String15("åäö_åäöå"); // fits in 15 chars 522 } 523 } 524 525 /// scope checked string 526 pure unittest 527 { 528 enum capacity = 15; 529 foreach (Str; AliasSeq!(StringN!capacity, 530 WStringN!capacity, 531 DStringN!capacity)) 532 { 533 static assert(!mustAddGCRange!Str); 534 static if (isDIP1000) 535 { 536 static assert(!__traits(compiles, { 537 auto f() @safe pure 538 { 539 auto x = Str("alphas"); 540 auto y = x[]; 541 return y; // errors with -dip1000 542 } 543 })); 544 } 545 } 546 } 547 548 @safe pure unittest 549 { 550 static assert(mustAddGCRange!(FixedArray!(string, 1, false))); 551 static assert(mustAddGCRange!(FixedArray!(string, 1, true))); 552 static assert(mustAddGCRange!(FixedArray!(string, 2, false))); 553 static assert(mustAddGCRange!(FixedArray!(string, 2, true))); 554 } 555 556 /// 557 @safe pure unittest 558 { 559 import std.exception : assertNotThrown; 560 561 alias T = char; 562 enum capacity = 3; 563 564 alias A = FixedArray!(T, capacity, true); 565 static assert(!mustAddGCRange!A); 566 static assert(A.sizeof == T.sizeof*capacity + 1); 567 568 import std.range.primitives : isOutputRange; 569 static assert(isOutputRange!(A, T)); 570 571 auto ab = A("ab"); 572 assert(!ab.empty); 573 assert(ab[0] == 'a'); 574 assert(ab.front == 'a'); 575 assert(ab.back == 'b'); 576 assert(ab.length == 2); 577 assert(ab[] == "ab"); 578 assert(ab[0 .. 1] == "a"); 579 assertNotThrown(ab.insertBack('_')); 580 assert(ab[] == "ab_"); 581 ab.popBack(); 582 assert(ab[] == "ab"); 583 assert(ab.toString == "ab"); 584 585 ab.popBackN(2); 586 assert(ab.empty); 587 assertNotThrown(ab.insertBack('a', 'b')); 588 589 const abc = A("abc"); 590 assert(!abc.empty); 591 assert(abc.front == 'a'); 592 assert(abc.back == 'c'); 593 assert(abc.length == 3); 594 assert(abc[] == "abc"); 595 assert(ab[0 .. 2] == "ab"); 596 assert(abc.full); 597 static assert(!__traits(compiles, { const abcd = A('a', 'b', 'c', 'd'); })); // too many elements 598 599 assert(ab[] == "ab"); 600 ab.popFront(); 601 assert(ab[] == "b"); 602 603 const xy = A("xy"); 604 assert(!xy.empty); 605 assert(xy[0] == 'x'); 606 assert(xy.front == 'x'); 607 assert(xy.back == 'y'); 608 assert(xy.length == 2); 609 assert(xy[] == "xy"); 610 assert(xy[0 .. 1] == "x"); 611 612 const xyz = A("xyz"); 613 assert(!xyz.empty); 614 assert(xyz.front == 'x'); 615 assert(xyz.back == 'z'); 616 assert(xyz.length == 3); 617 assert(xyz[] == "xyz"); 618 assert(xyz.full); 619 static assert(!__traits(compiles, { const xyzw = A('x', 'y', 'z', 'w'); })); // too many elements 620 } 621 622 /// 623 @safe pure unittest 624 { 625 static void testAsSomeString(T)() 626 { 627 enum capacity = 15; 628 alias A = FixedArray!(immutable(T), capacity); 629 static assert(!mustAddGCRange!A); 630 auto a = A("abc"); 631 assert(a[] == "abc"); 632 assert(a[].equal("abc")); 633 634 import std.conv : to; 635 const x = "a".to!(T[]); 636 } 637 638 foreach (T; AliasSeq!(char// , wchar, dchar 639 )) 640 { 641 testAsSomeString!T(); 642 } 643 } 644 645 /// equality 646 @safe pure unittest 647 { 648 enum capacity = 15; 649 alias S = FixedArray!(int, capacity); 650 static assert(!mustAddGCRange!S); 651 652 assert(S([1, 2, 3].s[]) == 653 S([1, 2, 3].s[])); 654 assert(S([1, 2, 3].s[]) == 655 [1, 2, 3]); 656 } 657 658 @safe pure unittest 659 { 660 class C { int value; } 661 alias S = FixedArray!(C, 2); 662 static assert(mustAddGCRange!S); 663 } 664 665 /// `insertBackMaybe` is nothrow @nogc. 666 @safe pure nothrow @nogc unittest 667 { 668 alias S = FixedArray!(int, 2); 669 S s; 670 assert(s.insertBackMaybe(42)); 671 assert(s.insertBackMaybe(43)); 672 assert(!s.insertBackMaybe(0)); 673 assert(s.length == 2); 674 } 675 676 /// equality 677 @system pure nothrow @nogc unittest 678 { 679 enum capacity = 15; 680 alias S = FixedArray!(int, capacity); 681 682 assert(S.fromValuesUnsafe([1, 2, 3].s) == 683 S.fromValuesUnsafe([1, 2, 3].s)); 684 685 const ax = [1, 2, 3].s; 686 assert(S.fromValuesUnsafe([1, 2, 3].s) == ax); 687 assert(S.fromValuesUnsafe([1, 2, 3].s) == ax[]); 688 689 const cx = [1, 2, 3].s; 690 assert(S.fromValuesUnsafe([1, 2, 3].s) == cx); 691 assert(S.fromValuesUnsafe([1, 2, 3].s) == cx[]); 692 693 immutable ix = [1, 2, 3].s; 694 assert(S.fromValuesUnsafe([1, 2, 3].s) == ix); 695 assert(S.fromValuesUnsafe([1, 2, 3].s) == ix[]); 696 } 697 698 /// assignment from `const` to `immutable` element type 699 @safe pure unittest 700 { 701 enum capacity = 15; 702 alias String15 = StringN!(capacity); 703 static assert(!mustAddGCRange!String15); 704 705 const char[4] _ = ['a', 'b', 'c', 'd']; 706 auto x = String15(_[]); 707 assert(x.length == 4); 708 assert(x[] == "abcd"); 709 } 710 711 /// borrow checking 712 @system pure unittest 713 { 714 enum capacity = 15; 715 alias String15 = StringN!(capacity, true); 716 static assert(String15.readBorrowCountMax == 7); 717 static assert(!mustAddGCRange!String15); 718 719 auto x = String15("alpha"); 720 721 assert(x[].equal("alpha") && 722 x[].equal("alpha")); 723 724 { 725 auto xw1 = x[]; 726 assert(x.isWriteBorrowed); 727 assert(x.isBorrowed); 728 } 729 730 auto xr1 = (cast(const)x)[]; 731 assert(x.readBorrowCount == 1); 732 733 auto xr2 = (cast(const)x)[]; 734 assert(x.readBorrowCount == 2); 735 736 auto xr3 = (cast(const)x)[]; 737 assert(x.readBorrowCount == 3); 738 739 auto xr4 = (cast(const)x)[]; 740 assert(x.readBorrowCount == 4); 741 742 auto xr5 = (cast(const)x)[]; 743 assert(x.readBorrowCount == 5); 744 745 auto xr6 = (cast(const)x)[]; 746 assert(x.readBorrowCount == 6); 747 748 auto xr7 = (cast(const)x)[]; 749 assert(x.readBorrowCount == 7); 750 751 assertThrown!AssertError((cast(const)x)[]); 752 } 753 754 version(unittest) 755 { 756 import std.algorithm.comparison : equal; 757 import std.meta : AliasSeq; 758 import std.exception : assertThrown; 759 import core.exception : AssertError; 760 761 import nxt.array_help : s; 762 import nxt.container_traits : mustAddGCRange; 763 import nxt.dip_traits : isDIP1000; 764 }