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`
11  *
12  * TODO: merge with variant_arrays.d?
13  * TODO: Maybe growth logic can be hidden inside a wrapper Allocator
14  *
15  * See_Also: http://forum.dlang.org/post/wvulryummkqtskiwrusb@forum.dlang.org
16  * See_Also: https://forum.dlang.org/post/purhollnapramxczmcka@forum.dlang.org
17  * See_Also: https://forum.dlang.org/post/cvxuagislrpfomalcelc@forum.dlang.org
18  * See_Also: https://maikklein.github.io/post/soa-d/
19  */
20 module nxt.container.soa;
21 
22 import std.experimental.allocator.mallocator : Mallocator;
23 import std.experimental.allocator.common : isAllocator;
24 
25 /** Structure of Arrays similar to members of `S`.
26  */
27 struct SoA(S, Capacity = size_t, Allocator = Mallocator)
28 if (is(S == struct) &&		  /+ TODO: extend to `isAggregate!S`? +/
29 	isAllocator!Allocator) {
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 		this(in Capacity initialCapacity, Allocator allocator) {
44 			_capacity = initialCapacity;
45 			this.allocator = allocator;
46 			allocate(initialCapacity);
47 		}
48 		this() @disable; ///< No default construction if an allocator must be provided.
49 	} else
50 		this(in Capacity initialCapacity) {
51 			_capacity = initialCapacity;
52 			allocate(initialCapacity);
53 		}
54 
55 	this(this) @disable;	// disable copy constructor
56 
57 	~this() nothrow @trusted @nogc {
58 		import std.experimental.allocator : dispose;
59 		static foreach (const index, _; S.tupleof)
60 			allocator.dispose(getArray!index);
61 	}
62 
63 	pragma(inline, true):
64 
65 	Capacity length() const @property pure nothrow @safe @nogc => _length;
66 	Capacity capacity() const @property pure nothrow @safe @nogc => _capacity;
67 	bool empty() const @property pure nothrow @safe @nogc => _length == 0;
68 
69 	inout(SoAElementRef!S) opIndex()(in Capacity elementIndex) inout return /*tlm*/
70 	in(elementIndex < _length)
71 		=> typeof(return)(&this, elementIndex);
72 
73 	inout(SoASlice!S) opSlice()() inout return /*tlm*/
74 		=> typeof(return)(&this);
75 
76 	void opOpAssign(string op, S)(S value) if (op == "~") => insertBack(value);
77 
78 	pragma(inline):				// reset
79 
80 	auto opDispatch(string name)() {
81 		static foreach (const index, memberSymbol; S.tupleof)
82 			static if (name == memberSymbol.stringof)
83 				return getArray!index;
84 		/+ TODO: static assert(0, S.stringof ~ " has no field named " ~ name); +/
85 	}
86 
87 	/** Push element (struct) `value` to back of array. */
88 	void insertBack()(S value) @trusted /*tlm*/ {
89 		import core.lifetime : moveEmplace;
90 		reserveOneExtra();
91 		static foreach (const index, memberSymbol; S.tupleof)
92 			moveEmplace(__traits(getMember, value, memberSymbol.stringof),
93 						getArray!index[_length]); /+ TODO: assert that +/
94 		++_length;
95 	}
96 
97 	/** Push element `value` to back of array using its data members `members`. */
98 	void insertBackMembers()(Types members) @trusted /*tlm*/ {
99 		import core.lifetime : moveEmplace;
100 		reserveOneExtra();
101 		// move each member to its position respective array
102 		static foreach (const index, _; members)
103 			moveEmplace(members[index], getArray!index[_length]); // same as `getArray!index[_length] = members[index];`
104 		++_length;
105 	}
106 
107 private:
108 	import nxt.allocator_traits : AllocatorState;
109 	mixin AllocatorState!Allocator; // put first as emsi-containers do
110 
111 	// generate array definitions
112 	static foreach (const index, Type; Types)
113 		mixin(Type.stringof ~ `[] _container` ~ index.stringof ~ ";");
114 
115 	/** Get array of all fields at aggregate field index `index`. */
116 	pragma(inline, true)
117 	ref inout(Types[index][]) getArray(Capacity index)() inout return {
118 		mixin(`return _container` ~ index.stringof ~ ";");
119 	}
120 
121 	Capacity _length = 0;		 ///< Current length.
122 	Capacity _capacity = 0;	   ///< Current capacity.
123 
124 	void allocate(in Capacity newCapacity) @trusted
125 	{
126 		import std.experimental.allocator : makeArray;
127 		static foreach (const index, _; S.tupleof)
128 			getArray!index = allocator.makeArray!(Types[index])(newCapacity);
129 	}
130 
131 	/** Grow storage with at least on element. */
132 	void grow() @trusted
133 	{
134 		// Motivation: https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md#memory-handling
135 		import std.algorithm.comparison : max;
136 		import std.experimental.allocator : expandArray;
137 		const newCapacity = _capacity == 1 ? 2 : max(1, _growthP * _capacity / _growthQ);
138 		const expandSize = newCapacity - _capacity;
139 		if (_capacity is 0)
140 			allocate(newCapacity);
141 		else
142 			static foreach (const index, _; S.tupleof)
143 				allocator.expandArray(getArray!index, expandSize);
144 		_capacity = newCapacity;
145 	}
146 
147 	void reserveOneExtra() {
148 		if (_length == _capacity)
149 			grow();
150 	}
151 }
152 alias StructArrays = SoA;
153 
154 /** Reference to element in `soaPtr` at index `elementIndex`. */
155 private struct SoAElementRef(S)
156 if (is(S == struct)) /+ TODO: extend to `isAggregate!S`? +/ {
157 	SoA!S* soaPtr;
158 	size_t elementIndex;
159 
160 	this(this) @disable;
161 
162 	/** Access member name `memberName`. */
163 	auto ref opDispatch(string memberName)() @trusted return scope {
164 		mixin(`return ` ~ `(*soaPtr).` ~ memberName ~ `[elementIndex];`);
165 	}
166 }
167 
168 /** Reference to slice in `soaPtr`. */
169 private struct SoASlice(S)
170 if (is(S == struct))			/+ TODO: extend to `isAggregate!S`? +/
171 {
172 	SoA!S* soaPtr;
173 
174 	this(this) @disable;
175 
176 	/** Access aggregate at `index`. */
177 	inout(S) opIndex(in size_t index) inout @trusted return scope {
178 		S s = void;
179 		static foreach (const memberIndex, memberSymbol; S.tupleof)
180 			mixin(`s.` ~ memberSymbol.stringof ~ `= (*soaPtr).getArray!` ~ memberIndex.stringof ~ `[index];`);
181 		return s;
182 	}
183 }
184 
185 pure nothrow @safe @nogc unittest {
186 	import nxt.dip_traits : hasPreviewDIP1000;
187 
188 	struct S { int i; float f; }
189 
190 	auto x = SoA!S();
191 
192 	static assert(is(typeof(x.getArray!0()) == int[]));
193 	static assert(is(typeof(x.getArray!1()) == float[]));
194 
195 	assert(x.length == 0);
196 
197 	x.insertBack(S.init);
198 	assert(x.length == 1);
199 
200 	x ~= S.init;
201 	assert(x.length == 2);
202 
203 	x.insertBackMembers(42, 43f);
204 	assert(x.length == 3);
205 	assert(x.i[2] == 42);
206 	assert(x.f[2] == 43f);
207 
208 	// uses opDispatch
209 	assert(x[2].i == 42);
210 	assert(x[2].f == 43f);
211 
212 	const x3 = SoA!S(3);
213 	assert(x3.length == 0);
214 	assert(x3.capacity == 3);
215 
216 	/+ TODO: make foreach work +/
217 	// foreach (_; x[])
218 	// {
219 	// }
220 
221 	static if (hasPreviewDIP1000) {
222 		static assert(!__traits(compiles,
223 								{
224 									ref int testScope() @safe
225 									{
226 										auto y = SoA!S(1);
227 										y ~= S(42, 43f);
228 										return y[0].i;
229 									}
230 								}));
231 	}
232 }