1 module nxt.owned;
2 
3 /** Return wrapper around container `Container` that can be safely sliced, by
4     tracking number of read borrowed ranges and whether it's currently write
5     borrowed.
6 
7     Only relevant when `Container` implements referenced access over
8     <ul>
9     <li> `opSlice` and
10     <li> `opIndex`
11     </ul>
12 
13     TODO Iterate and wrap all @unsafe accessors () and wrapped borrow
14     checks for all modifying members of `Container`?
15 */
16 struct Owned(Container)
17     if (needsOwnership!Container)
18 {
19     import std.range.primitives : hasSlicing;
20     import std.traits : isMutable;
21 
22     /// Type of range of `Container`.
23     alias Range = typeof(Container.init[]);
24 
25 pragma(inline):
26 
27     ~this() @nogc
28     {
29         assert(!_writeBorrowed, "This is still write-borrowed, cannot release!");
30         assert(_readBorrowCount == 0, "This is still read-borrowed, cannot release!");
31     }
32 
33     /// Move `this` into a returned r-value.
34     typeof(this) move()
35     {
36         assert(!_writeBorrowed, "This is still write-borrowed, cannot move!");
37         assert(_readBorrowCount == 0, "This is still read-borrowed, cannot move!");
38         import core.lifetime : move;
39         return move(this);
40     }
41 
42     /** Checked overload for `std.algorithm.mutation.move`. */
43     void move(ref typeof(this) dst) pure nothrow @nogc
44     {
45         assert(!_writeBorrowed, "Source is still write-borrowed, cannot move!");
46         assert(_readBorrowCount == 0, "Source is still read-borrowed, cannot move!");
47 
48         assert(!dst._writeBorrowed, "Destination is still write-borrowed, cannot move!");
49         assert(dst._readBorrowCount == 0, "Destination is still read-borrowed, cannot move!");
50 
51         import core.lifetime : move;
52         move(this, dst);
53     }
54 
55     /** Checked overload for `std.algorithm.mutation.moveEmplace`. */
56     void moveEmplace(ref typeof(this) dst) pure nothrow @nogc
57     {
58         assert(!_writeBorrowed, "Source is still write-borrowed, cannot moveEmplace!");
59         assert(_readBorrowCount == 0, "Source is still read-borrowed, cannot moveEmplace!");
60 
61         import core.lifetime : moveEmplace;
62         moveEmplace(this, dst);
63     }
64 
65     static if (true/*TODO hasUnsafeSlicing!Container*/)
66     {
67         import nxt.borrowed : ReadBorrowed, WriteBorrowed;
68 
69         // TODO can all these definitions be reduce somehow?
70 
71         /// Get full read-only slice.
72         ReadBorrowed!(Range, Owned) sliceRO() const @trusted
73         {
74             import core.internal.traits : Unqual;
75             assert(!_writeBorrowed, "This is already write-borrowed");
76             return typeof(return)(_container.opSlice,
77                                   cast(Unqual!(typeof(this))*)(&this)); // trusted unconst casta
78         }
79 
80         /// Get read-only slice in range `i` .. `j`.
81         ReadBorrowed!(Range, Owned) sliceRO(size_t i, size_t j) const @trusted
82         {
83             import core.internal.traits : Unqual;
84             assert(!_writeBorrowed, "This is already write-borrowed");
85             return typeof(return)(_container.opSlice[i .. j],
86                                   cast(Unqual!(typeof(this))*)(&this)); // trusted unconst cast
87         }
88 
89         /// Get full read-write slice.
90         WriteBorrowed!(Range, Owned) sliceRW() @trusted
91         {
92             assert(!_writeBorrowed, "This is already write-borrowed");
93             assert(_readBorrowCount == 0, "This is already read-borrowed");
94             return typeof(return)(_container.opSlice, &this);
95         }
96 
97         /// Get read-write slice in range `i` .. `j`.
98         WriteBorrowed!(Range, Owned) sliceRW(size_t i, size_t j) @trusted
99         {
100             assert(!_writeBorrowed, "This is already write-borrowed");
101             assert(_readBorrowCount == 0, "This is already read-borrowed");
102             return typeof(return)(_container.opSlice[i .. j], &this);
103         }
104 
105         /// Get read-only slice in range `i` .. `j`.
106         auto opSlice(size_t i, size_t j) const
107         {
108             return sliceRO(i, j);
109         }
110         /// Get read-write slice in range `i` .. `j`.
111         auto opSlice(size_t i, size_t j)
112         {
113             return sliceRW(i, j);
114         }
115 
116         /// Get read-only full slice.
117         auto opSlice() const
118         {
119             return sliceRO();
120         }
121         /// Get read-write full slice.
122         auto opSlice()
123         {
124             return sliceRW();
125         }
126     }
127 
128     @safe pure nothrow @nogc:
129 
130     @property:
131 
132     /// Returns: `true` iff `this` is either write or read borrowed.
133     bool isBorrowed() const { return _writeBorrowed || _readBorrowCount >= 1; }
134 
135     /// Returns: `true` iff owned container is write borrowed.
136     bool isWriteBorrowed() const { return _writeBorrowed; }
137 
138     /// Returns: number of read-only borrowers of owned container.
139     uint readBorrowCount() const { return _readBorrowCount; }
140 
141     Container _container;            /// wrapped container
142     alias _container this;
143 
144 public:
145     bool _writeBorrowed = false; /// `true` iff `_container` is currently referred to
146     uint _readBorrowCount = 0; /// number of readable borrowers. TODO use `size_t` minus one bit instead in `size_t _stats`
147     enum readBorrowCountMax = typeof(_readBorrowCount).max;
148 }
149 
150 /** Checked overload for `std.algorithm.mutation.move`.
151 
152     TODO Can we somehow prevent users of Owned from accidentally using
153     `std.algorithm.mutation.move` instead of this wrapper?
154  */
155 void move(Owner)(ref Owner src, ref Owner dst) @safe pure nothrow @nogc
156     if (isInstanceOf!(Owned, Owner))
157 {
158     src.move(dst);              // reuse member function
159 }
160 
161 /** Checked overload for `std.algorithm.mutation.moveEmplace`.
162 
163     TODO Can we somehow prevent users of Owned from accidentally using
164     `std.algorithm.mutation.moveEmplace` instead of this wrapper?
165 */
166 void moveEmplace(Owner)(ref Owner src, ref Owner dst) @safe pure nothrow @nogc
167     if (isInstanceOf!(Owned, Owner))
168 {
169     src.moveEmplace(dst);   // reuse member function
170 }
171 
172 template needsOwnership(Container)
173 {
174     import std.range.primitives : hasSlicing;
175     // TODO activate when array_ex : UniqueArray
176     // enum needsOwnership = hasSlicing!Container; // TODO extend to check if it's not @safe
177     enum needsOwnership = is(Container == struct);
178 }
179 
180 pure unittest
181 {
182     alias A = UniqueArray!int;
183     const Owned!A co;          // const owner
184 
185     import std.traits : isMutable;
186     static assert(!isMutable!(typeof(co)));
187 
188     const cos = co[];
189 }
190 
191 @safe pure unittest
192 {
193     alias A = UniqueArray!int;
194     A a = A.init;
195     a = A.init;
196     // TODO a ~= A.init;
197 }
198 
199 pure unittest
200 {
201     import std.traits : isInstanceOf;
202     import std.exception: assertThrown;
203     import core.exception : AssertError;
204 
205     import nxt.borrowed : ReadBorrowed, WriteBorrowed;
206 
207     alias A = UniqueArray!int;
208 
209     Owned!A oa;
210 
211     Owned!A ob;
212     oa.move(ob);                // ok to move unborrowed
213 
214     Owned!A od = void;
215     oa.moveEmplace(od);         // ok to moveEmplace unborrowed
216 
217     oa ~= 1;
218     oa ~= 2;
219     assert(oa[] == [1, 2]);
220     assert(oa[0 .. 1] == [1]);
221     assert(oa[1 .. 2] == [2]);
222     assert(oa[0 .. 2] == [1, 2]);
223     assert(!oa.isWriteBorrowed);
224     assert(!oa.isBorrowed);
225     assert(oa.readBorrowCount == 0);
226 
227     {
228         const wb = oa.sliceRW;
229 
230         Owned!A oc;
231         assertThrown!AssertError(oa.move()); // cannot move write borrowed
232 
233         assert(wb.length == 2);
234         static assert(!__traits(compiles, { auto wc = wb; })); // write borrows cannot be copied
235         assert(oa.isBorrowed);
236         assert(oa.isWriteBorrowed);
237         assert(oa.readBorrowCount == 0);
238         assertThrown!AssertError(oa.opSlice); // one more write borrow is not allowed
239     }
240 
241     // ok to write borrow again in separate scope
242     {
243         const wb = oa.sliceRW;
244 
245         assert(wb.length == 2);
246         assert(oa.isBorrowed);
247         assert(oa.isWriteBorrowed);
248         assert(oa.readBorrowCount == 0);
249     }
250 
251     // ok to write borrow again in separate scope
252     {
253         const wb = oa.sliceRW(0, 2);
254         assert(wb.length == 2);
255         assert(oa.isBorrowed);
256         assert(oa.isWriteBorrowed);
257         assert(oa.readBorrowCount == 0);
258     }
259 
260     // multiple read-only borrows are allowed
261     {
262         const rb1 = oa.sliceRO;
263 
264         Owned!A oc;
265         assertThrown!AssertError(oa.move(oc)); // cannot move read borrowed
266 
267         assert(rb1.length == oa.length);
268         assert(oa.readBorrowCount == 1);
269 
270         const rb2 = oa.sliceRO;
271         assert(rb2.length == oa.length);
272         assert(oa.readBorrowCount == 2);
273 
274         const rb3 = oa.sliceRO;
275         assert(rb3.length == oa.length);
276         assert(oa.readBorrowCount == 3);
277 
278         const rb_ = rb3;
279         assert(rb_.length == oa.length);
280         assert(oa.readBorrowCount == 4);
281         assertThrown!AssertError(oa.sliceRW); // single write borrow is not allowed
282     }
283 
284     // test modification via write borrow
285     {
286         auto wb = oa.sliceRW;
287         wb[0] = 11;
288         wb[1] = 12;
289         assert(wb.length == oa.length);
290         assert(oa.isWriteBorrowed);
291         assert(oa.readBorrowCount == 0);
292         assertThrown!AssertError(oa.sliceRO);
293     }
294     assert(oa[] == [11, 12]);
295     assert(oa.sliceRO(0, 2) == [11, 12]);
296 
297     // test mutable slice
298     static assert(isInstanceOf!(WriteBorrowed, typeof(oa.sliceRW())));
299     static assert(isInstanceOf!(WriteBorrowed, typeof(oa[])));
300     foreach (ref e; oa.sliceRW)
301     {
302         assertThrown!AssertError(oa.sliceRO); // one more write borrow is not allowed
303         assertThrown!AssertError(oa.sliceRW); // one more write borrow is not allowed
304         assertThrown!AssertError(oa[]); // one more write borrow is not allowed
305     }
306 
307     // test readable slice
308     static assert(isInstanceOf!(ReadBorrowed, typeof(oa.sliceRO())));
309     foreach (const ref e; oa.sliceRO)
310     {
311         assert(oa.sliceRO.length == oa.length);
312         assert(oa.sliceRO[0 .. 0].length == 0);
313         assert(oa.sliceRO[0 .. 1].length == 1);
314         assert(oa.sliceRO[0 .. 2].length == oa.length);
315         assertThrown!AssertError(oa.sliceRW); // write borrow during iteration is not allowed
316         assertThrown!AssertError(oa.move());  // move not allowed when borrowed
317     }
318 
319     // move semantics
320     auto oaMove1 = oa.move();
321     auto oaMove2 = oaMove1.move();
322     assert(oaMove2[] == [11, 12]);
323 
324     // constness propagation from owner to borrower
325     Owned!A mo;          // mutable owner
326     assert(mo.sliceRO.ptr == mo.ptr);
327     assert(mo.sliceRO(0, 0).ptr == mo.ptr);
328     static assert(isInstanceOf!(ReadBorrowed, typeof(mo.sliceRO())));
329 
330     const Owned!A co;          // const owner
331     assert(co.sliceRO.ptr == co.ptr);
332     static assert(isInstanceOf!(ReadBorrowed, typeof(co.sliceRO())));
333 }
334 
335 nothrow unittest
336 {
337     import std.algorithm.sorting : sort;
338     alias E = int;
339     alias A = UniqueArray!E;
340     A a;
341     sort(a[]);         // TODO make this work
342 }
343 
344 // y = sort(x.move()), where x and y are instances of unsorted Array
345 @safe nothrow unittest
346 {
347     import std.algorithm.sorting : sort;
348     import std.range.primitives : isInputRange, isForwardRange, isRandomAccessRange, hasSlicing;
349 
350     alias E = int;
351     alias A = UniqueArray!E;
352     alias O = Owned!A;
353 
354     O o;
355     o ~= [42, 43];
356 
357     assert(o.length == 2);
358 
359     scope os = o.sliceRO;
360 
361     alias OS = typeof(os);
362     static assert(isInputRange!(OS));
363     static assert(isForwardRange!(OS));
364     static assert(hasSlicing!(OS));
365     static assert(isRandomAccessRange!OS);
366 
367     assert(!os.empty);
368     assert(os.length == 2);
369     os.popFront();
370     assert(!os.empty);
371     assert(os.length == 1);
372     os.popFront();
373     assert(os.empty);
374 
375     // TODO scope oss = os[];            // no op
376     // TODO assert(oss.empty);
377 }
378 
379 // check write-borrow
380 @safe nothrow unittest
381 {
382     import std.algorithm.sorting : sort;
383     import std.range.primitives : isInputRange, isForwardRange, isRandomAccessRange, hasSlicing;
384 
385     alias E = int;
386     alias A = UniqueArray!E;
387     alias O = Owned!A;
388 
389     const O co;
390     auto cos = co[0 .. 0];
391     const ccos = co[0 .. 0];
392 
393     // TODO const coc = co[].save();
394 
395     O o;
396     o ~= [42, 43];
397     auto os = o.sliceRW;
398     alias OS = typeof(os);
399     static assert(isInputRange!(OS));
400 
401     // static assert(isForwardRange!(OS));
402     // static assert(hasSlicing!(OS));
403     // static assert(isRandomAccessRange!OS);
404     // import std.algorithm.sorting : sort;
405     // sort(o[]);
406 }
407 
408 version(unittest)
409 {
410     import nxt.dynamic_array : UniqueArray = DynamicArray;
411     import nxt.dbgio;
412 }