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 <vladimir@thecybershadow.net>
12  */
13 
14 module ae.utils.appender;
15 
16 import std.experimental.allocator : makeArray, stateSize;
17 import std.experimental.allocator.common : stateSize;
18 import std.experimental.allocator.gc_allocator : GCAllocator;
19 import std.traits;
20 
21 struct FastAppender(I, Allocator = GCAllocator)
22 {
23 	static assert(T.sizeof == 1, "TODO");
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;
47 				extended = GC.extend(start, newSize, newSize * 2);
48 			}
49 			else
50 			{
51 				static if (is(hasMember!(Allocator, "expand")))
52 				{
53 					auto buf = start[0..capacity];
54 					if (allocator.expand(buf, newSize))
55 					{
56 						assert(buf.ptr == start);
57 						extended = buf.length;
58 					}
59 				}
60 			}
61 			if (extended)
62 			{
63 				end = start + extended;
64 				return;
65 			}
66 		}
67 
68 		auto newCapacity = newSize < MIN_SIZE ? MIN_SIZE : newSize * 2;
69 
70 		version(none)
71 		{
72 			auto bi = GC.qalloc(newCapacity * T.sizeof, (typeid(T[]).next.flags & 1) ? 0 : GC.BlkAttr.NO_SCAN);
73 			auto newStart = cast(T*)bi.base;
74 			newCapacity = bi.size;
75 		}
76 		else
77 		{
78 			auto buf = allocator.makeArray!T(newCapacity);
79 			assert(buf.length == newCapacity);
80 			auto newStart = buf.ptr;
81 		}
82 
83 		newStart[0..size] = start[0..size];
84 		if (unique)
85 			allocator.deallocate(start[0..capacity]);
86 		start = newStart;
87 		cursor = start + size;
88 		end = start + newCapacity;
89 		unique = true;
90 	}
91 
92 public:
93 	static if (stateSize!Allocator)
94 		Allocator allocator;
95 	else
96 		alias allocator = Allocator.instance;
97 
98 	static if (stateSize!Allocator == 0)
99 	{
100 		/// Preallocate
101 		this(size_t capacity)
102 		{
103 			reserve(capacity);
104 		}
105 	}
106 
107 	/// Start with a given buffer
108 	this(I[] arr)
109 	{
110 		start = cursor = cast(T*)arr.ptr;
111 		end = start + arr.length;
112 	}
113 
114 	@disable this(this);
115 
116 	~this()
117 	{
118 		if (cursor && unique)
119 			allocator.deallocate(start[0..end-start]);
120 	}
121 
122 	/// Put elements.
123 	/// Accepts any number of items (and will allocate at most once per call).
124 	/// Items can be of the element type (I), or arrays.
125 	void put(U...)(U items)
126 		if (CanPutAll!U)
127 	{
128 		// TODO: check for static if length is 1
129 		auto cursorEnd = cursor;
130 		foreach (item; items)
131 			static if (is(typeof(cursor[0] = item)))
132 				cursorEnd++;
133 			else
134 			// TODO: is this too lax? it allows passing static arrays by value
135 			static if (is(typeof(cursor[0..1] = item[0..1])))
136 				cursorEnd += item.length;
137 			else
138 				static assert(0, "Can't put " ~ typeof(item).stringof);
139 
140 		if (cursorEnd > end)
141 		{
142 			auto len = cursorEnd - cursor;
143 			reserve(len);
144 			cursorEnd = cursor + len;
145 		}
146 		auto cursor = this.cursor;
147 		this.cursor = cursorEnd;
148 
149 		static if (items.length == 1)
150 		{
151 			alias items[0] item;
152 			static if (is(typeof(cursor[0] = item)))
153 				cursor[0] = item;
154 			else
155 				cursor[0..item.length] = item[];
156 		}
157 		else
158 		{
159 			foreach (item; items)
160 				static if (is(typeof(cursor[0] = item)))
161 					*cursor++ = item;
162 				else
163 				static if (is(typeof(cursor[0..1] = item[0..1])))
164 				{
165 					cursor[0..item.length] = item[];
166 					cursor += item.length;
167 				}
168 		}
169 	}
170 
171 	/// Unsafe. Use together with preallocate().
172 	void uncheckedPut(U...)(U items) @system
173 		if (CanPutAll!U)
174 	{
175 		auto cursor = this.cursor;
176 
177 		foreach (item; items)
178 			static if (is(typeof(cursor[0] = item)))
179 				*cursor++ = item;
180 			else
181 			static if (is(typeof(cursor[0..1] = item[0..1])))
182 			{
183 				cursor[0..item.length] = item;
184 				cursor += item.length;
185 			}
186 
187 		this.cursor = cursor;
188 	}
189 
190 	/// Ensure we can append at least `len` more bytes before allocating.
191 	void preallocate(size_t len)
192 	{
193 		if (end - cursor < len)
194 			reserve(len);
195 	}
196 
197 	/// Allocate a number of bytes, without initializing them,
198 	/// and return the slice to be filled in.
199 	/// The slice reference is temporary, and valid until the next allocation.
200 	T[] allocate(size_t len) @system
201 	{
202 		auto cursorEnd = cursor + len;
203 		if (cursorEnd > end)
204 		{
205 			reserve(len);
206 			cursorEnd = cursor + len;
207 		}
208 		auto result = cursor[0..len];
209 		cursor = cursorEnd;
210 		return result;
211 	}
212 
213 	template CanPutAll(U...)
214 	{
215 		static if (U.length==0)
216 			enum bool CanPutAll = true;
217 		else
218 		{
219 			enum bool CanPutAll =
220 				(
221 					is(typeof(cursor[0   ] = U[0].init      )) ||
222 				 	is(typeof(cursor[0..1] = U[0].init[0..1]))
223 				) && CanPutAll!(U[1..$]);
224 		}
225 	}
226 
227 	void opOpAssign(string op, U)(U item)
228 		if (op=="~" && is(typeof(put!U)))
229 	{
230 		put(item);
231 	}
232 
233 	/// Get a reference to the buffer.
234 	/// Ownership of the buffer is passed to the caller
235 	/// (Appender will not deallocate it after this call).
236 	I[] get()
237 	{
238 		unique = false;
239 		return peek();
240 	}
241 
242 	/// As with `get`, but ownership is preserved.
243 	/// The return value is valid until the next allocation,
244 	/// or until Appender is destroyed.
245 	I[] peek()
246 	{
247 		return cast(I[])start[0..cursor-start];
248 	}
249 
250 	@property size_t capacity()
251 	{
252 		return end-start;
253 	}
254 
255 	@property void capacity(size_t value)
256 	{
257 		immutable current = end - start;
258 		assert(value >= current, "Cannot shrink capacity");
259 		reserve(value - length);
260 	}
261 
262 	@property size_t length()
263 	{
264 		return cursor-start;
265 	}
266 
267 	static if (is(I == T)) // mutable types only
268 	{
269 		/// Set the length (up to the current capacity).
270 		/// Does not resize. Use preallocate for that.
271 		@property void length(size_t value)
272 		{
273 			if (start + value > end)
274 				preallocate(start + value - end);
275 			cursor = start + value;
276 			assert(cursor <= end);
277 		}
278 
279 		/// Effectively empties the data, but preserves the storage for reuse.
280 		/// Same as setting length to 0.
281 		void clear()
282 		{
283 			cursor = start;
284 		}
285 	}
286 }
287 
288 unittest
289 {
290 	import std.meta : AliasSeq;
291 	import std.experimental.allocator.mallocator;
292 
293 	foreach (Allocator; AliasSeq!(GCAllocator, Mallocator))
294 	{
295 		FastAppender!(char, Allocator) a;
296 		assert(a.get == "");
297 		a.put('a', "bcd", 'e');
298 		assert(a.get == "abcde");
299 		a.clear();
300 		assert(a.get == "");
301 		a.allocate(3)[] = 'x';
302 		assert(a.get == "xxx");
303 	}
304 }