1 /**
2  * Optimized copying appender, no chaining
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <ae@cy.md>
12  */
13 
14 module ae.utils.appender;
15 
16 import std.algorithm.comparison : max;
17 import std.experimental.allocator : makeArray, stateSize;
18 import std.experimental.allocator.common : stateSize;
19 import std.experimental.allocator.gc_allocator : GCAllocator;
20 import std.traits;
21 
22 /// Optimized appender. Not copyable.
23 struct FastAppender(I, Allocator = GCAllocator)
24 {
25 private:
26 	enum PAGE_SIZE = 4096;
27 	enum MIN_SIZE  = PAGE_SIZE / 2 + 1; // smallest size that can expand
28 
29 	alias Unqual!I T;
30 
31 	T* cursor, start, end;
32 	bool unique; // Holding a unique reference to the buffer?
33 
34 	void reserve(size_t len)
35 	{
36 		immutable size = cursor-start;
37 		immutable newSize = size + len;
38 		immutable capacity = end-start;
39 
40 		if (start)
41 		{
42 			size_t extended = 0;
43 			static if (is(Allocator == GCAllocator))
44 			{
45 				// std.allocator does not have opportunistic extend
46 				import core.memory : GC;
47 				extended = GC.extend(start, newSize * T.sizeof, newSize * 2 * T.sizeof) / T.sizeof;
48 			}
49 			else
50 			{
51 				static if (hasMember!(Allocator, "expand"))
52 				{
53 					void[] buf = start[0..capacity];
54 					if (allocator.expand(buf, newSize * T.sizeof))
55 					{
56 						assert(buf.ptr == start);
57 						extended = buf.length / T.sizeof;
58 					}
59 				}
60 			}
61 			if (extended)
62 			{
63 				end = start + extended;
64 				return;
65 			}
66 		}
67 
68 		enum minSize = max(1, MIN_SIZE / T.sizeof);
69 		auto newCapacity = newSize < minSize ? minSize : newSize * 2;
70 
71 		version(none)
72 		{
73 			auto bi = GC.qalloc(newCapacity * T.sizeof, (typeid(T[]).next.flags & 1) ? 0 : GC.BlkAttr.NO_SCAN);
74 			auto newStart = cast(T*)bi.base;
75 			newCapacity = bi.size;
76 		}
77 		else
78 		{
79 			auto buf = allocator.makeArray!T(newCapacity);
80 			assert(buf.length == newCapacity);
81 			auto newStart = buf.ptr;
82 		}
83 
84 		newStart[0..size] = start[0..size];
85 		if (unique)
86 			allocator.deallocate(start[0..capacity]);
87 		start = newStart;
88 		cursor = start + size;
89 		end = start + newCapacity;
90 		unique = true;
91 	}
92 
93 public:
94 	/// The allocator.
95 	static if (stateSize!Allocator)
96 		Allocator allocator;
97 	else
98 		alias allocator = Allocator.instance;
99 
100 	static if (stateSize!Allocator == 0)
101 	{
102 		/// Preallocate
103 		this(size_t capacity)
104 		{
105 			reserve(capacity);
106 		}
107 	}
108 
109 	/// Start with a given buffer
110 	this(I[] arr)
111 	{
112 		start = cursor = cast(T*)arr.ptr;
113 		end = start + arr.length;
114 	}
115 
116 	@disable this(this);
117 
118 	~this()
119 	{
120 		if (cursor && unique)
121 			allocator.deallocate(start[0..end-start]);
122 	}
123 
124 	/// Put elements.
125 	/// Accepts any number of items (and will allocate at most once per call).
126 	/// Items can be of the element type (I), or arrays.
127 	void putEx(U...)(U items)
128 		if (CanPutAll!U)
129 	{
130 		// TODO: check for static if length is 1
131 		auto cursorEnd = cursor;
132 		foreach (item; items)
133 			static if (is(typeof(cursor[0] = item)))
134 				cursorEnd++;
135 			else
136 			// TODO: is this too lax? it allows passing static arrays by value
137 			static if (is(typeof(cursor[0..1] = item[0..1])))
138 				cursorEnd += item.length;
139 			else
140 				static assert(0, "Can't put " ~ typeof(item).stringof);
141 
142 		if (cursorEnd > end)
143 		{
144 			auto len = cursorEnd - cursor;
145 			reserve(len);
146 			cursorEnd = cursor + len;
147 		}
148 		auto cursor = this.cursor;
149 		this.cursor = cursorEnd;
150 
151 		static if (items.length == 1)
152 		{
153 			alias items[0] item;
154 			static if (is(typeof(cursor[0] = item)))
155 				cursor[0] = item;
156 			else
157 				cursor[0..item.length] = item[];
158 		}
159 		else
160 		{
161 			foreach (item; items)
162 				static if (is(typeof(cursor[0] = item)))
163 					*cursor++ = item;
164 				else
165 				static if (is(typeof(cursor[0..1] = item[0..1])))
166 				{
167 					cursor[0..item.length] = item[];
168 					cursor += item.length;
169 				}
170 		}
171 	}
172 
173 	alias put = putEx; /// Output range shim.
174 
175 	/// Unsafe. Use together with preallocate().
176 	void uncheckedPut(U...)(U items) @system
177 		if (CanPutAll!U)
178 	{
179 		auto cursor = this.cursor;
180 
181 		foreach (item; items)
182 			static if (is(typeof(cursor[0] = item)))
183 				*cursor++ = item;
184 			else
185 			static if (is(typeof(cursor[0..1] = item[0..1])))
186 			{
187 				cursor[0..item.length] = item;
188 				cursor += item.length;
189 			}
190 
191 		this.cursor = cursor;
192 	}
193 
194 	/// Ensure we can append at least `len` more bytes before allocating.
195 	void preallocate(size_t len)
196 	{
197 		if (end - cursor < len)
198 			reserve(len);
199 	}
200 
201 	/// Allocate a number of bytes, without initializing them,
202 	/// and return the slice to be filled in.
203 	/// The slice reference is temporary, and valid until the next allocation.
204 	T[] allocate(size_t len) @system
205 	{
206 		auto cursorEnd = cursor + len;
207 		if (cursorEnd > end)
208 		{
209 			reserve(len);
210 			cursorEnd = cursor + len;
211 		}
212 		auto result = cursor[0..len];
213 		cursor = cursorEnd;
214 		return result;
215 	}
216 
217 	private template CanPutAll(U...)
218 	{
219 		static if (U.length==0)
220 			enum bool CanPutAll = true;
221 		else
222 		{
223 			enum bool CanPutAll =
224 				(
225 					is(typeof(cursor[0   ] = U[0].init      )) ||
226 				 	is(typeof(cursor[0..1] = U[0].init[0..1]))
227 				) && CanPutAll!(U[1..$]);
228 		}
229 	}
230 
231 	/// `~=` support.
232 	void opOpAssign(string op, U)(U item)
233 		if (op=="~" && is(typeof(put!U)))
234 	{
235 		put(item);
236 	}
237 
238 	/// Get a reference to the buffer.
239 	/// Ownership of the buffer is passed to the caller
240 	/// (Appender will not deallocate it after this call).
241 	I[] get()
242 	{
243 		unique = false;
244 		return peek();
245 	}
246 
247 	/// As with `get`, but ownership is preserved.
248 	/// The return value is valid until the next allocation,
249 	/// or until Appender is destroyed.
250 	I[] peek()
251 	{
252 		return cast(I[])start[0..cursor-start];
253 	}
254 
255 	/// How many items can be appended without a reallocation.
256 	@property size_t capacity()
257 	{
258 		return end-start;
259 	}
260 
261 	/// Resize backing buffer to the given capacity.
262 	@property void capacity(size_t value)
263 	{
264 		immutable current = end - start;
265 		assert(value >= current, "Cannot shrink capacity");
266 		reserve(value - length);
267 	}
268 
269 	/// How many items have been written so far.
270 	@property size_t length()
271 	{
272 		return cursor-start;
273 	}
274 
275 	static if (is(I == T)) // mutable types only
276 	{
277 		/// Set the length (up to the current capacity).
278 		@property void length(size_t value)
279 		{
280 			if (start + value > end)
281 				preallocate(start + value - end);
282 			cursor = start + value;
283 			assert(cursor <= end);
284 		}
285 
286 		/// Effectively empties the data, but preserves the storage for reuse.
287 		/// Same as setting length to 0.
288 		void clear()
289 		{
290 			cursor = start;
291 		}
292 	}
293 }
294 
295 unittest
296 {
297 	import std.meta : AliasSeq;
298 	import std.experimental.allocator.mallocator;
299 
300 	foreach (Allocator; AliasSeq!(GCAllocator, Mallocator))
301 		foreach (C; AliasSeq!(char, wchar, dchar))
302 		{
303 			FastAppender!(C, Allocator) a;
304 			assert(a.get == "");
305 			immutable C[] s = "bcd";
306 			a.put(C('a'), s, C('e'));
307 			assert(a.get == "abcde");
308 			a.clear();
309 			assert(a.get == "");
310 			a.allocate(3)[] = 'x';
311 			assert(a.get == "xxx");
312 		}
313 }
314 
315 /// UFCS shim for classic output ranges, which only take a single-argument put.
316 void putEx(R, U...)(auto ref R r, U items)
317 {
318 	foreach (item; items)
319 	{
320 		static if (is(typeof(r.put(item))))
321 			r.put(item);
322 		else
323 		static if (is(typeof(r.put(item[]))))
324 			r.put(item[]);
325 		else
326 		static if (is(typeof({ foreach (c; item) r.put(c); })))
327 			foreach (c; item)
328 				r.put(c);
329 		else
330 		static if (is(typeof(r.put((&item)[0..1]))))
331 			r.put((&item)[0..1]);
332 		else
333 			static assert(false, "Can't figure out how to put " ~ typeof(item).stringof ~ " into a " ~ R.stringof);
334 	}
335 }