1 /**
2    Implements a memory block to place elements on.
3 */
4 
5 module alid.blockreusable;
6 
7 import alid.errornogc : NogcError;
8 import std.typecons : Flag, Yes;
9 
10 // The Error type that ReusableBlock throws
11 private mixin NogcError!"block";
12 
13 /**
14    A reusable memory block for placing objects on.
15 
16    The elements are added only at the back either by copying or by moving. They
17    are removed only from the front. The elements are never moved to a different
18    memory location inside the buffer.
19 
20    Params:
21 
22        T = the type of the elements that will be stored on the block
23        dtors = whether to execute destructors of elements upon removing them
24 */
25 struct ReusableBlock(T, Flag!"dtors" dtors = Yes.dtors)
26 {
27 private:
28 
29     T * ptr_;           // The address of the beginning of the block
30     size_t capacity_;   // Total elements that the block can hold
31     size_t head_;       // The block index where element 0 is currently at
32     size_t tail_;       // The block index where the next element will be placed at
33 
34 public:
35 
36     /**
37         Construct an object that will use the provided memory
38 
39         Params:
40 
41             buffer = the memory _buffer to use for the elements; the `.ptr` property
42                      must match `T.alignof`
43     */
44     this(ubyte[] buffer) @nogc nothrow pure scope @trusted
45     {
46         const extra = cast(ulong)buffer.ptr % T.alignof;
47         if (extra) {
48             // Align to the next aligned memory location
49             buffer = buffer[T.alignof - extra .. $];
50         }
51         assert(cast(ulong)buffer.ptr % T.alignof == 0);
52 
53         this.ptr_ = cast(T*)(buffer.ptr);
54         this.capacity_ = buffer.length / T.sizeof;
55         this.head_ = 0;
56         this.tail_ = 0;
57     }
58 
59     /// Pointer to the beginning of the block
60     inout(T) * ptr() inout @nogc nothrow pure @safe scope
61     {
62         return ptr_;
63     }
64 
65     /// Total _capacity of the block
66     size_t capacity() const @nogc nothrow pure @safe scope
67     {
68         return capacity_;
69     }
70 
71     /// Number of elements the block currently has room for
72     size_t freeCapacity() const @nogc nothrow pure @safe scope
73     in (tail_ <= capacity_, blockError("Tail is ahead of capacity", this))
74     {
75         return capacity - tail_;
76     }
77 
78     /// Current number of elements in the block
79     size_t length() const @nogc nothrow pure @safe scope
80     in (head_ <= tail_, blockError("Head is ahead of tail", this))
81     {
82         return tail_ - head_;
83     }
84 
85     /// Whether the block has no elements at all
86     bool empty() const @nogc nothrow pure @safe scope
87     {
88         return length == 0;
89     }
90 
91     /**
92         Add an _element after the existing elements; lvalues are copied, rvalues
93         are moved.
94 
95         Params:
96 
97             element = the _element to add
98     */
99     void opOpAssign(string op, SourceT)(auto ref SourceT element)
100     if (op == "~")
101     in (freeCapacity, blockError("No room to append", this))
102     {
103         import std.traits : isCopyable;
104 
105         // This code is copied from std.array.Appender.put:
106         auto elementUnqual = (() @trusted => & cast() element)();
107 
108         static if (__traits(isRef, element))
109         {
110             // We have an lvalue
111             import core.lifetime : emplace;
112             import std.format : format;
113 
114             static assert(isCopyable!SourceT,
115                           format!"%s is not copyable"(SourceT.stringof));
116 
117             emplace(unqualPtr_ + tail_, *elementUnqual);
118             ++tail_;
119         }
120         else
121         {
122             // We have an rvalue
123             this.moveEmplace(element);
124         }
125     }
126 
127     /**
128         Calls `~=` to copy the specified _element
129 
130         Params:
131 
132             element = the _element to copy
133     */
134     void copyBack(SourceT)(auto ref const(SourceT) element)
135     {
136         this ~= element;
137     }
138 
139     /**
140         Move the specified _element to the back of existing elements
141 
142         Params:
143 
144             element = the _element to move
145     */
146     void moveEmplace(SourceT)(ref SourceT element)
147     in (freeCapacity, blockError("No room to moveEmplaceBack", this))
148     {
149         import core.lifetime : moveEmplace;
150 
151         // This code is copied from std.array.Appender.put:
152         auto elementUnqual = (() @trusted => & cast() element)();
153 
154         moveEmplace(*elementUnqual, *(unqualPtr_ + tail_));
155         ++tail_;
156     }
157 
158     /**
159         Construct a new element after the existing elements
160 
161         Params:
162 
163             args = the constructor arguments to use
164     */
165     void emplaceBack(Args...)(auto ref Args args)
166     in (freeCapacity, blockError("No room to emplaceBack", this))
167     {
168         import core.lifetime : emplace;
169         import std.format : format;
170 
171         static assert(__traits(compiles, emplace(ptr_, args)),
172                       format!"%s is not copyable"(T.stringof));
173 
174         emplace(ptr_ + tail_, args);
175         ++tail_;
176     }
177 
178     /**
179         Return a reference to an element
180 
181         Params:
182 
183             index = the _index of the element to return
184     */
185     ref inout(T) opIndex(in size_t index) inout @nogc nothrow pure scope
186     in (index < length, blockError("Invalid index", index, length))
187     {
188         return ptr_[head_ + index];
189     }
190 
191     /**
192         Remove elements from the head of the block. If `n == length`,
193         `freeCapacity` will be equal to `capacity` after this call. Whether the
194         destructors are called on the removed elements is determined by the
195         `dtors` template parameter.
196 
197         Params:
198 
199             n = number of elements to remove
200     */
201     void removeFrontN(in size_t n) scope
202     in (n <= length, blockError("Not enough elements to removeFrontN", n, this))
203     {
204         import std.traits : hasElaborateDestructor;
205 
206         static if (dtors && hasElaborateDestructor!T)
207         {
208             foreach_reverse(i; head_ .. head_ + n)
209             {
210                 destroy(unqualPtr_[i]);
211             }
212         }
213 
214         if (n == length)
215         {
216             // No element left; start from the beginning
217             head_ = 0;
218             tail_ = 0;
219         }
220         else
221         {
222             head_ += n;
223         }
224     }
225 
226     /// The same as calling `this.removeFrontN(this.length)`
227     void clear() scope
228     {
229         removeFrontN(length);
230     }
231 
232     /// Number of elements in the block
233     size_t opDollar() const @nogc nothrow pure @safe scope
234     {
235         return length;
236     }
237 
238     /**
239         A slice providing access _to the specified elements
240 
241         Params:
242 
243             from = the index of the first element of the slice
244             to = the index of the element one beyond the last element of the slice
245     */
246     inout(T)[] opSlice(in size_t from, in size_t to) inout @nogc nothrow pure scope
247     in (from <= to, blockError("Range begin is greater than end", from, to))
248     in (to - from <= length, blockError("Range is too long", from, to, length))
249     {
250         return ptr[head_ + from .. head_ + to];
251     }
252 
253     /// A slice to all elements; the same as `[0..$]`
254     inout(T)[] opSlice() inout @nogc nothrow pure scope
255     {
256         return ptr[head_ .. tail_];
257     }
258 
259     /// String representation of this object mostly for debugging
260     void toString(scope void delegate(in char[]) sink) const scope
261     {
262         import std.format : formattedWrite;
263 
264         sink.formattedWrite!"%s @%s cap:%s elems:[%s..%s]"(
265             T.stringof, ptr_, capacity_, head_, tail_);
266     }
267 
268     private auto unqualPtr_() inout @nogc nothrow pure scope
269     {
270         import std.traits : Unqual;
271 
272         return cast(Unqual!T*)ptr_;
273     }
274 }
275 
276 ///
277 unittest
278 {
279     // Constructing a block on a piece of memory
280     ubyte[100] buffer;
281     auto b = ReusableBlock!int(buffer);
282 
283     // Depending on the alignment of the elements, the capacity may be less than
284     // then requested amount
285     assert(b.capacity <= buffer.length / int.sizeof);
286 
287     // Add 2 elements
288     b ~= 0;
289     b ~= 1;
290 
291     b.length.shouldBe(2);
292     b[0].shouldBe(0);
293     b[1].shouldBe(1);
294     b.freeCapacity.shouldBe(b.capacity - 2);
295 
296     // Remove the first one
297     b.removeFrontN(1);
298     b.length.shouldBe(1);
299     b[0].shouldBe(1);
300     // Note that free capacity does not increase:
301     b.freeCapacity.shouldBe(b.capacity - 2);
302 
303     // Remove all elements and reset the block
304     b.clear();
305     b.length.shouldBe(0);
306     // This time all capacity is free
307     b.freeCapacity.shouldBe(b.capacity);
308 }
309 
310 version (unittest)
311 {
312     import alid.test : shouldBe;
313 
314     private void assertInitialState(B)(B b)
315     {
316         import std.array : empty;
317 
318         b.capacity.shouldBe(b.freeCapacity);
319         b.length.shouldBe(0);
320         assert(b.empty);
321         assert(b[0..$].empty);
322         assert(b[].empty);
323     }
324 
325     private auto makeValue(T)(int i)
326     if (is (T == string))
327     {
328         import std.format : format;
329 
330         return format!"value_%s"(i);
331     }
332 
333     private auto makeValue(T)(int i)
334     if (!is (T == string))
335     {
336         return cast(T)i;
337     }
338 }
339 
340 unittest
341 {
342     // Initializing with null buffer should not be an error
343 
344     auto b = ReusableBlock!int(null);
345     assertInitialState(b);
346 }
347 
348 unittest
349 {
350     // Test with some fundamental types
351 
352     import std.array : empty;
353     import std.meta : AliasSeq;
354 
355     void test(T)()
356     {
357         // Must be large enough to pass range tests below
358         enum length = 40 * int.sizeof;
359         ubyte[length] buffer;
360         auto b = ReusableBlock!T(buffer);
361         const cap = b.capacity;
362 
363         assertInitialState(b);
364 
365         // Add 1 element
366         const e = makeValue!T(42);
367         b ~= e;
368         b.capacity.shouldBe(cap);
369         b.freeCapacity.shouldBe(b.capacity - 1);
370         b.length.shouldBe(1);
371         assert(!b.empty);
372         b[0].shouldBe(e);
373         b[0..$].shouldBe([ e ]);
374         b[].shouldBe([ e ]);
375 
376         // Drop the element
377         b.removeFrontN(1);
378         b.capacity.shouldBe(cap);
379 
380         // As all elements are removed, free capacity is increased automatically
381         b.freeCapacity.shouldBe(b.capacity);
382         b.length.shouldBe(0);
383         assert(b.empty);
384         assert(b[0..$].empty);
385         assert(b[].empty);
386 
387         // Add another one
388         const f = makeValue!T(43);
389         b ~= f;
390         b.capacity.shouldBe(cap);
391 
392         // Note that free capacity is reduced
393         b.freeCapacity.shouldBe(b.capacity - 1);
394         b.length.shouldBe(1);
395         assert(!b.empty);
396         b[0].shouldBe(f);
397         b[0..$].shouldBe([ f ]);
398         b[].shouldBe([ f ]);
399 
400         // Clearing should get us back to the beginning
401         b.clear();
402         assertInitialState(b);
403 
404         // Fill test
405         T[] expected;
406         foreach (i; 0 .. b.capacity)
407         {
408             import std.conv : to;
409 
410             assert(b.freeCapacity);
411             const elem = makeValue!T(i.to!int);
412             b ~= elem;
413             expected ~= elem;
414         }
415 
416         // Maximum capacity should not change
417         b.capacity.shouldBe(cap);
418         b.freeCapacity.shouldBe(0);
419         b.length.shouldBe(b.capacity);
420         assert(!b.empty);
421         b[0..$].shouldBe(expected);
422         b[].shouldBe(expected);
423 
424         b.removeFrontN(7);
425         b.length.shouldBe(b.capacity - 7);
426         b[].shouldBe(expected[7..$]);
427         b[0..$].shouldBe(expected[7..$]);
428         b[2..$-1].shouldBe(expected[9..$-1]);
429 
430         // Multiple clear calls
431         b.clear();
432         b.clear();
433         assertInitialState(b);
434     }
435 
436     alias Ts = AliasSeq!(int, const(int), double, immutable(double), ubyte, string);
437 
438     foreach (T; Ts)
439     {
440         test!T();
441     }
442 }
443 
444 unittest
445 {
446     import std.meta : AliasSeq;
447 
448     struct S
449     {
450         int i = int.max;
451         int j = int.max;
452 
453         this(int i, int j)
454         {
455             this.i = i;
456             this.j = j;
457         }
458 
459         this(this) @nogc pure @safe scope {}
460     }
461 
462     void test(T)()
463     {
464         enum length = 400;
465         auto buffer = new ubyte[length];
466         auto b = ReusableBlock!T(buffer);
467 
468         b ~= T(2, 2);
469 
470         const n = T(7, 7);
471         b~= n;
472 
473         b.copyBack(n);
474         b.copyBack(T(8, 8));
475         b.copyBack(n);
476 
477         auto m = T(3, 3);
478         b.moveEmplace(m);
479         assert(m == S.init);
480     }
481 
482     alias Ts = AliasSeq!(S, const(S));
483 
484     foreach (T; Ts)
485     {
486         test!T();
487     }
488 }
489 
490 unittest
491 {
492     // Destructors should be executed when requested
493 
494     import std.algorithm : each;
495     import std.format : format;
496     import std.range : iota;
497     import std.typecons : No;
498 
499     void test(Flag!"dtors" dtors)(size_t elementCount, size_t expectedDtorCount)
500     {
501         static struct S
502         {
503             size_t * count = null;
504 
505             ~this()
506             {
507                 if (count)
508                 {
509                     ++(*count);
510                 }
511             }
512         }
513 
514         auto b = ReusableBlock!(S, dtors)(new ubyte[100]);
515 
516         size_t count = 0;
517         assert(count == 0, "Invalid unittest");
518 
519         iota(elementCount).each!(_ => b ~= S(&count));
520         b.removeFrontN(elementCount);
521 
522         count.shouldBe(expectedDtorCount);
523     }
524 
525     test!(Yes.dtors)(10, 10);
526     test!(No.dtors)(10, 0);
527 }