1 /** Structure of arrays (SoA).
2  *
3  * SoAs are common in game engines.
4  *
5  * Initially a builtin feature in the Jai programming language that later was
6  * made into a library solution.
7  *
8  * TODO merge with soa_petar_kirov.d by
9  * 1. allocate all arrays in a single chunk
10  * 2. calculating `_capacity` based on `_length` and
11  *
12  * TODO Maybe growth logic can be hidden inside a wrapper Allocator
13  *
14  * See_Also: http://forum.dlang.org/post/wvulryummkqtskiwrusb@forum.dlang.org
15  * See_Also: https://forum.dlang.org/post/purhollnapramxczmcka@forum.dlang.org
16  * See_Also: https://forum.dlang.org/post/cvxuagislrpfomalcelc@forum.dlang.org
17  * See_Also: https://maikklein.github.io/post/soa-d/
18  */
19 module nxt.soa;
20 
21 import std.experimental.allocator.mallocator : Mallocator;
22 import nxt.allocator_traits : isAllocator;
23 
24 /** Structure of arrays similar to members of `S`.
25  */
26 struct SoA(S, Capacity = size_t, Allocator = Mallocator)
27 if (is(S == struct) &&          // TODO: extend to `isAggregate!S`?
28     isAllocator!Allocator)
29 {
30     /** Growth factor P/Q.
31         https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md#memory-handling
32         Use 1.5 like Facebook's `fbvector` does.
33     */
34     enum _growthP = 3;          // numerator
35     /// ditto
36     enum _growthQ = 2;          // denominator
37 
38     private alias toType(string s) = typeof(__traits(getMember, S, s));
39     private alias Types = typeof(S.tupleof);
40 
41     import std.experimental.allocator.common : stateSize;
42     static if (stateSize!Allocator != 0)
43     {
44         this(in Capacity initialCapacity, Allocator allocator)
45         {
46             _capacity = initialCapacity;
47             this.allocator = allocator;
48             allocate(initialCapacity);
49         }
50 
51         this() @disable; ///< No default construction if an allocator must be provided.
52     }
53     else
54         this(in Capacity initialCapacity)
55         {
56             _capacity = initialCapacity;
57             allocate(initialCapacity);
58         }
59 
60     @disable this(this);    // disable copy constructor
61 
62     auto opDispatch(string name)()
63     {
64         static foreach (const index, memberSymbol; S.tupleof)
65             static if (name == memberSymbol.stringof)
66                 return getArray!index;
67         // TODO: static assert(0, S.stringof ~ " has no field named " ~ name);
68     }
69 
70     /** Push element (struct) `value` to back of array. */
71     void insertBack()(S value) @trusted // template-lazy
72     {
73         import core.lifetime : moveEmplace;
74         reserveOneExtra();
75         static foreach (const index, memberSymbol; S.tupleof)
76             moveEmplace(__traits(getMember, value, memberSymbol.stringof),
77                         getArray!index[_length]); // TODO: assert that
78         ++_length;
79     }
80 
81     /** Push element (struct) `value` to back of array using its data members `members`. */
82     void insertBackMembers()(Types members) @trusted // template-lazy
83     {
84         import core.lifetime : moveEmplace;
85         reserveOneExtra();
86         // move each member to its position respective array
87         static foreach (const index, _; members)
88             moveEmplace(members[index], getArray!index[_length]); // same as `getArray!index[_length] = members[index];`
89         ++_length;
90     }
91 
92     void opOpAssign(string op, S)(S value)
93     if (op == "~")
94     {
95         pragma(inline, true);
96         insertBack(value);
97     }
98 
99     /** Length of this array. */
100     @property Capacity length() const @safe pure nothrow @nogc
101     {
102         return _length;
103     }
104 
105     /** Capacity of this array. */
106     @property Capacity capacity() const @safe pure nothrow @nogc
107     {
108         return _capacity;
109     }
110 
111     /** Returns true iff no elements are present. */
112     bool empty() const @safe pure nothrow @nogc { return _length == 0; }
113 
114     ~this() @trusted @nogc
115     {
116         import std.experimental.allocator : dispose;
117         static foreach (const index, _; S.tupleof)
118             allocator.dispose(getArray!index);
119     }
120 
121     /** Index operator. */
122     inout(SoAElementRef!S) opIndex()(in Capacity elementIndex) inout return // template-lazy
123     {
124         assert(elementIndex < _length);
125         return typeof(return)(&this, elementIndex);
126     }
127 
128     /** Slice operator. */
129     inout(SoASlice!S) opSlice()() inout return // template-lazy
130     {
131         return typeof(return)(&this);
132     }
133 
134 private:
135     import nxt.allocator_traits : AllocatorState;
136     mixin AllocatorState!Allocator; // put first as emsi-containers do
137 
138     // generate array definitions
139     static foreach (const index, Type; Types)
140         mixin(Type.stringof ~ `[] _container` ~ index.stringof ~ ";");
141 
142     /** Get array of all fields at aggregate field index `index`. */
143     ref inout(Types[index][]) getArray(Capacity index)() inout return
144     {
145         mixin(`return _container` ~ index.stringof ~ ";");
146     }
147 
148     Capacity _length = 0;         ///< Current length.
149     Capacity _capacity = 0;       ///< Current capacity.
150 
151     void allocate(in Capacity newCapacity) @trusted
152     {
153         import std.experimental.allocator : makeArray;
154         static foreach (const index, _; S.tupleof)
155             getArray!index = allocator.makeArray!(Types[index])(newCapacity);
156     }
157 
158     /** Grow storage with at least on element.
159      */
160     void grow() @trusted
161     {
162         // Motivation: https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md#memory-handling
163         import std.algorithm.comparison : max;
164         import std.experimental.allocator : expandArray;
165         const newCapacity = _capacity == 1 ? 2 : max(1, _growthP * _capacity / _growthQ);
166         const expandSize = newCapacity - _capacity;
167         if (_capacity is 0)
168             allocate(newCapacity);
169         else
170             static foreach (const index, _; S.tupleof)
171                 allocator.expandArray(getArray!index, expandSize);
172         _capacity = newCapacity;
173     }
174 
175     void reserveOneExtra()
176     {
177         if (_length == _capacity)
178             grow();
179     }
180 }
181 alias StructArrays = SoA;
182 
183 /** Reference to element in `soaPtr` at index `elementIndex`. */
184 private struct SoAElementRef(S)
185 if (is(S == struct))        // TODO: extend to `isAggregate!S`?
186 {
187     SoA!S* soaPtr;
188     size_t elementIndex;
189 
190     @disable this(this);
191 
192     /** Access member name `memberName`. */
193     auto ref opDispatch(string memberName)()
194         @trusted return scope
195     {
196         mixin(`return ` ~ `(*soaPtr).` ~ memberName ~ `[elementIndex];`);
197     }
198 }
199 
200 /** Reference to slice in `soaPtr`. */
201 private struct SoASlice(S)
202 if (is(S == struct))            // TODO: extend to `isAggregate!S`?
203 {
204     SoA!S* soaPtr;
205 
206     @disable this(this);
207 
208     /** Access aggregate at `index`. */
209     inout(S) opIndex(in size_t index) inout @trusted return scope
210     {
211         S s = void;
212         static foreach (const memberIndex, memberSymbol; S.tupleof)
213             mixin(`s.` ~ memberSymbol.stringof ~ `= (*soaPtr).getArray!` ~ memberIndex.stringof ~ `[index];`);
214         return s;
215     }
216 }
217 
218 @safe pure nothrow @nogc unittest
219 {
220     import nxt.dip_traits : isDIP1000;
221 
222     struct S { int i; float f; }
223 
224     auto x = SoA!S();
225 
226     static assert(is(typeof(x.getArray!0()) == int[]));
227     static assert(is(typeof(x.getArray!1()) == float[]));
228 
229     assert(x.length == 0);
230 
231     x.insertBack(S.init);
232     assert(x.length == 1);
233 
234     x ~= S.init;
235     assert(x.length == 2);
236 
237     x.insertBackMembers(42, 43f);
238     assert(x.length == 3);
239     assert(x.i[2] == 42);
240     assert(x.f[2] == 43f);
241 
242     // uses opDispatch
243     assert(x[2].i == 42);
244     assert(x[2].f == 43f);
245 
246     const x3 = SoA!S(3);
247     assert(x3.length == 0);
248     assert(x3.capacity == 3);
249 
250     // TODO: make foreach work
251     // foreach (_; x[])
252     // {
253     // }
254 
255     static if (isDIP1000)
256     {
257         static assert(!__traits(compiles,
258                                 {
259                                     ref int testScope() @safe
260                                     {
261                                         auto y = SoA!S(1);
262                                         y ~= S(42, 43f);
263                                         return y[0].i;
264                                     }
265                                 }));
266     }
267 }