1 /** Structure of arrays similar to builtin feature in the Jai programming language.
2  *
3  * See_Also: https://maikklein.github.io/post/soa-d/
4  * See_Also: http://forum.dlang.org/post/wvulryummkqtskiwrusb@forum.dlang.org
5  * See_Also: https://forum.dlang.org/post/purhollnapramxczmcka@forum.dlang.org
6  */
7 module nxt.soa;
8 
9 /** Structure of arrays similar to members of `S`.
10  */
11 struct SOA(S)
12 if (is(S == struct))        // TODO extend to `isAggregate!S`?
13 {
14     import core.lifetime : move, moveEmplace;
15     import nxt.pure_mallocator : PureMallocator;
16 
17     private alias toType(string s) = typeof(__traits(getMember, S, s));
18     private alias Types = typeof(S.tupleof);
19 
20     this(size_t initialCapacity)
21     {
22         _capacity = initialCapacity;
23         allocate(initialCapacity);
24     }
25 
26     auto opDispatch(string name)()
27     {
28         static foreach (index, memberSymbol; S.tupleof)
29         {
30             static if (name == memberSymbol.stringof)
31             {
32                 return getArray!index;
33             }
34         }
35         // TODO static assert(0, S.stringof ~ " has no field named " ~ name);
36     }
37 
38     /// Push element (struct) `value` to back of array.
39     void insertBack()(S value) @trusted // template-lazy
40     {
41         reserveOneExtra();
42         static foreach (const index, memberSymbol; S.tupleof)
43         {
44             moveEmplace(__traits(getMember, value, memberSymbol.stringof),
45                         getArray!index[_length]); // TODO assert that
46         }
47         ++_length;
48     }
49 
50     /// Push element (struct) `value` to back of array using its data members `members`.
51     void insertBackMembers()(Types members) @trusted // template-lazy
52     {
53         reserveOneExtra();
54         // move each member to its position respective array
55         static foreach (const index, _; members)
56         {
57             moveEmplace(members[index], getArray!index[_length]); // same as `getArray!index[_length] = members[index];`
58         }
59         ++_length;
60     }
61 
62     void opOpAssign(string op, S)(S value)
63         if (op == "~")
64     {
65         pragma(inline, true);
66         insertBack(move(value));      // TODO remove when compiler does this for us
67     }
68 
69     /// Length of this array.
70     @property size_t length() const @safe pure nothrow @nogc
71     {
72         return _length;
73     }
74 
75     /// Capacity of this array.
76     @property size_t capacity() const @safe pure nothrow @nogc
77     {
78         return _capacity;
79     }
80 
81     ~this() @trusted @nogc
82     {
83         import std.experimental.allocator : dispose;
84         static foreach (const index, _; S.tupleof)
85         {
86             PureMallocator.instance.dispose(getArray!index);
87         }
88     }
89 
90     /** Index operator. */
91     inout(SOAElementRef!S) opIndex()(size_t elementIndex) inout return // template-lazy
92     {
93         assert(elementIndex < _length);
94         return typeof(return)(&this, elementIndex);
95     }
96 
97     /** Slice operator. */
98     inout(SOASlice!S) opSlice()() inout return // template-lazy
99     {
100         return typeof(return)(&this);
101     }
102 
103 private:
104 
105     // generate array definitions
106     static foreach (index, Type; Types)
107     {
108         mixin(Type.stringof ~ `[] _container` ~ index.stringof ~ ";");
109     }
110 
111     /// Get array of all fields at aggregate field index `index`.
112     ref inout(Types[index][]) getArray(size_t index)() inout return
113     {
114         mixin(`return _container` ~ index.stringof ~ ";");
115     }
116 
117     size_t _length = 0;
118     size_t _capacity = 0;
119     short _growthFactor = 2;
120 
121     void allocate(size_t newCapacity) @trusted
122     {
123         // if (_alloc is null)
124         // {
125         //     _alloc = allocatorObject(Mallocator.instance);
126         // }
127         import std.experimental.allocator : makeArray;
128         static foreach (const index, _; S.tupleof)
129         {
130             getArray!index = PureMallocator.instance.makeArray!(Types[index])(newCapacity);
131         }
132     }
133 
134     void grow() @trusted
135     {
136         import std.algorithm.comparison : max;
137         const newCapacity = max(1, _capacity * _growthFactor);
138         const expandSize = newCapacity - _capacity;
139 
140         if (_capacity is 0)
141         {
142             allocate(newCapacity);
143         }
144         else
145         {
146             import std.experimental.allocator : expandArray;
147             static foreach (const index, _; S.tupleof)
148             {
149                 PureMallocator.instance.expandArray(getArray!index, expandSize);
150             }
151         }
152         _capacity = newCapacity;
153     }
154 
155     void reserveOneExtra()
156     {
157         if (_length == _capacity) { grow(); }
158     }
159 }
160 alias StructArrays = SOA;
161 
162 /// Reference to element in `soaPtr` at index `elementIndex`.
163 private struct SOAElementRef(S)
164 if (is(S == struct))        // TODO extend to `isAggregate!S`?
165 {
166     SOA!S* soaPtr;
167     size_t elementIndex;
168 
169     @disable this(this);
170 
171     /// Access member name `memberName`.
172     auto ref opDispatch(string memberName)()
173         @trusted return scope
174     {
175         mixin(`return ` ~ `(*soaPtr).` ~ memberName ~ `[elementIndex];`);
176     }
177 }
178 
179 /// Reference to slice in `soaPtr`.
180 private struct SOASlice(S)
181     if (is(S == struct))        // TODO extend to `isAggregate!S`?
182 {
183     SOA!S* soaPtr;
184 
185     @disable this(this);
186 
187     /// Access aggregate at `index`.
188     inout(S) opIndex(size_t index) inout @trusted return scope
189     {
190         S s = void;
191         static foreach (memberIndex, memberSymbol; S.tupleof)
192         {
193             mixin(`s.` ~ memberSymbol.stringof ~ `= (*soaPtr).getArray!` ~ memberIndex.stringof ~ `[index];`);
194         }
195         return s;
196     }
197 }
198 
199 @safe:
200 
201 @safe pure nothrow @nogc unittest
202 {
203     import nxt.dip_traits : isDIP1000;
204 
205     struct S { int i; float f; }
206 
207     auto x = SOA!S();
208 
209     static assert(is(typeof(x.getArray!0()) == int[]));
210     static assert(is(typeof(x.getArray!1()) == float[]));
211 
212     assert(x.length == 0);
213 
214     x.insertBack(S.init);
215     assert(x.length == 1);
216 
217     x ~= S.init;
218     assert(x.length == 2);
219 
220     x.insertBackMembers(42, 43f);
221     assert(x.length == 3);
222     assert(x.i[2] == 42);
223     assert(x.f[2] == 43f);
224 
225     // uses opDispatch
226     assert(x[2].i == 42);
227     assert(x[2].f == 43f);
228 
229     const x3 = SOA!S(3);
230     assert(x3.length == 0);
231     assert(x3.capacity == 3);
232 
233     // TODO make foreach work
234     // foreach (_; x[])
235     // {
236     // }
237 
238     static if (isDIP1000)
239     {
240         static assert(!__traits(compiles,
241                                 {
242                                     ref int testScope() @safe
243                                     {
244                                         auto y = SOA!S(1);
245                                         y ~= S(42, 43f);
246                                         return y[0].i;
247                                     }
248                                 }));
249     }
250 }