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() nothrow @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 (is(Owner == Owned!(_), _))
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 (is(Owner == Owned!(_), _))
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.exception: assertThrown;
202     import core.exception : AssertError;
203 
204     import nxt.borrowed : ReadBorrowed, WriteBorrowed;
205 
206     alias A = UniqueArray!int;
207 
208     Owned!A oa;
209 
210     Owned!A ob;
211     oa.move(ob);                // ok to move unborrowed
212 
213     Owned!A od = void;
214     oa.moveEmplace(od);         // ok to moveEmplace unborrowed
215 
216     oa ~= 1;
217     oa ~= 2;
218     assert(oa[] == [1, 2]);
219     assert(oa[0 .. 1] == [1]);
220     assert(oa[1 .. 2] == [2]);
221     assert(oa[0 .. 2] == [1, 2]);
222     assert(!oa.isWriteBorrowed);
223     assert(!oa.isBorrowed);
224     assert(oa.readBorrowCount == 0);
225 
226     {
227         const wb = oa.sliceRW;
228 
229         Owned!A oc;
230         assertThrown!AssertError(oa.move()); // cannot move write borrowed
231 
232         assert(wb.length == 2);
233         static assert(!__traits(compiles, { auto wc = wb; })); // write borrows cannot be copied
234         assert(oa.isBorrowed);
235         assert(oa.isWriteBorrowed);
236         assert(oa.readBorrowCount == 0);
237         assertThrown!AssertError(oa.opSlice); // one more write borrow is not allowed
238     }
239 
240     // ok to write borrow again in separate scope
241     {
242         const wb = oa.sliceRW;
243 
244         assert(wb.length == 2);
245         assert(oa.isBorrowed);
246         assert(oa.isWriteBorrowed);
247         assert(oa.readBorrowCount == 0);
248     }
249 
250     // ok to write borrow again in separate scope
251     {
252         const wb = oa.sliceRW(0, 2);
253         assert(wb.length == 2);
254         assert(oa.isBorrowed);
255         assert(oa.isWriteBorrowed);
256         assert(oa.readBorrowCount == 0);
257     }
258 
259     // multiple read-only borrows are allowed
260     {
261         const rb1 = oa.sliceRO;
262 
263         Owned!A oc;
264         assertThrown!AssertError(oa.move(oc)); // cannot move read borrowed
265 
266         assert(rb1.length == oa.length);
267         assert(oa.readBorrowCount == 1);
268 
269         const rb2 = oa.sliceRO;
270         assert(rb2.length == oa.length);
271         assert(oa.readBorrowCount == 2);
272 
273         const rb3 = oa.sliceRO;
274         assert(rb3.length == oa.length);
275         assert(oa.readBorrowCount == 3);
276 
277         const rb_ = rb3;
278         assert(rb_.length == oa.length);
279         assert(oa.readBorrowCount == 4);
280         assertThrown!AssertError(oa.sliceRW); // single write borrow is not allowed
281     }
282 
283     // test modification via write borrow
284     {
285         auto wb = oa.sliceRW;
286         wb[0] = 11;
287         wb[1] = 12;
288         assert(wb.length == oa.length);
289         assert(oa.isWriteBorrowed);
290         assert(oa.readBorrowCount == 0);
291         assertThrown!AssertError(oa.sliceRO);
292     }
293     assert(oa[] == [11, 12]);
294     assert(oa.sliceRO(0, 2) == [11, 12]);
295 
296     // test mutable slice
297     static assert(is(typeof(oa.sliceRW()) == WriteBorrowed!(_), _...));
298     static assert(is(typeof(oa[]) == WriteBorrowed!(_), _...));
299     foreach (ref e; oa.sliceRW)
300     {
301         assertThrown!AssertError(oa.sliceRO); // one more write borrow is not allowed
302         assertThrown!AssertError(oa.sliceRW); // one more write borrow is not allowed
303         assertThrown!AssertError(oa[]); // one more write borrow is not allowed
304     }
305 
306     // test readable slice
307     static assert(is(typeof(oa.sliceRO()) == ReadBorrowed!(_), _...));
308     foreach (const ref e; oa.sliceRO)
309     {
310         assert(oa.sliceRO.length == oa.length);
311         assert(oa.sliceRO[0 .. 0].length == 0);
312         assert(oa.sliceRO[0 .. 1].length == 1);
313         assert(oa.sliceRO[0 .. 2].length == oa.length);
314         assertThrown!AssertError(oa.sliceRW); // write borrow during iteration is not allowed
315         assertThrown!AssertError(oa.move());  // move not allowed when borrowed
316     }
317 
318     // move semantics
319     auto oaMove1 = oa.move();
320     auto oaMove2 = oaMove1.move();
321     assert(oaMove2[] == [11, 12]);
322 
323     // constness propagation from owner to borrower
324     Owned!A mo;          // mutable owner
325     assert(mo.sliceRO.ptr == mo.ptr);
326     assert(mo.sliceRO(0, 0).ptr == mo.ptr);
327     static assert(is(typeof(mo.sliceRO()) == ReadBorrowed!(_), _...));
328 
329     const Owned!A co;          // const owner
330     assert(co.sliceRO.ptr == co.ptr);
331     static assert(is(typeof(co.sliceRO()) == ReadBorrowed!(_), _...));
332 }
333 
334 nothrow unittest
335 {
336     import std.algorithm.sorting : sort;
337     alias E = int;
338     alias A = UniqueArray!E;
339     A a;
340     sort(a[]);         // TODO: make this work
341 }
342 
343 // y = sort(x.move()), where x and y are instances of unsorted Array
344 @safe nothrow unittest
345 {
346     import std.algorithm.sorting : sort;
347     import std.range.primitives : isInputRange, isForwardRange, isRandomAccessRange, hasSlicing;
348 
349     alias E = int;
350     alias A = UniqueArray!E;
351     alias O = Owned!A;
352 
353     O o;
354     o ~= [42, 43];
355 
356     assert(o.length == 2);
357 
358     scope os = o.sliceRO;
359 
360     alias OS = typeof(os);
361     static assert(isInputRange!(OS));
362     static assert(isForwardRange!(OS));
363     static assert(hasSlicing!(OS));
364     static assert(isRandomAccessRange!OS);
365 
366     assert(!os.empty);
367     assert(os.length == 2);
368     os.popFront();
369     assert(!os.empty);
370     assert(os.length == 1);
371     os.popFront();
372     assert(os.empty);
373 
374     // TODO: scope oss = os[];            // no op
375     // TODO: assert(oss.empty);
376 }
377 
378 // check write-borrow
379 @safe nothrow unittest
380 {
381     import std.algorithm.sorting : sort;
382     import std.range.primitives : isInputRange, isForwardRange, isRandomAccessRange, hasSlicing;
383 
384     alias E = int;
385     alias A = UniqueArray!E;
386     alias O = Owned!A;
387 
388     const O co;
389     auto cos = co[0 .. 0];
390     const ccos = co[0 .. 0];
391 
392     // TODO: const coc = co[].save();
393 
394     O o;
395     o ~= [42, 43];
396     auto os = o.sliceRW;
397     alias OS = typeof(os);
398     static assert(isInputRange!(OS));
399 
400     // static assert(isForwardRange!(OS));
401     // static assert(hasSlicing!(OS));
402     // static assert(isRandomAccessRange!OS);
403     // import std.algorithm.sorting : sort;
404     // sort(o[]);
405 }
406 
407 version(unittest)
408 {
409     import nxt.container.dynamic_array : UniqueArray = DynamicArray;
410     import nxt.dbgio;
411 }