1 module libd.async.task;
2 
3 import libd.async.coroutine;
4 import libd.datastructures : TypedPtr, makeTyped;
5 import libd.memory : move;
6 import libd.util : BcError, raise, displayError;
7 
8 enum TaskState
9 {
10     uninit,
11     running,
12     yielded,
13     errored,
14     done
15 }
16 
17 private struct TaskContext
18 {
19     TypedPtr      userContext;
20     TypedPtr      taskYieldValue;
21     CoroutineFunc entryPoint;
22     BcError       error;
23     bool          yieldedWithValue;
24 }
25 
26 struct Task
27 {
28     private Coroutine*     _coroutine;
29     private TaskState      _state;
30     private TaskContext    _context;
31     private CoroutineStack _stack;
32 
33     @disable this(this){}
34 
35     @nogc nothrow:
36 
37     this(CoroutineFunc func)
38     {
39         this(func, null);
40     }
41 
42     this(T)(CoroutineFunc func, auto ref T context)
43     {
44         static if(!is(T == typeof(null)))
45             this._context.userContext = context.makeTyped;
46 
47         // TODO: Stack customisation. New PageAllocator means memory is managed a lot better now.
48         this._stack = coroutineCreateStandaloneStack();
49         this._context.entryPoint = func;
50         this._coroutine = coroutineCreate(&coroutine, this._stack, &this._context);
51     }
52 
53     this(ref return scope Task rhs)
54     {
55         this._coroutine = rhs._coroutine;
56         this._state     = rhs._state;
57         move(rhs._context, this._context);
58         this._stack     = rhs._stack;
59         if(this._coroutine !is null)
60             this._coroutine.context = &this._context;
61     }
62 
63     private static void coroutine()
64     {
65         auto ctx = cast(TaskContext*)coroutineGetContext();
66         assert(ctx !is null, "This was not called during a task. Tasks are a more focused layer placed on top of coroutines.");
67         assert(ctx.entryPoint !is null, "No/null entrypoint was given.");
68         version(D_BetterC)
69             ctx.entryPoint();
70         else // So unittests don't completely crash on failure (most of the time).
71         {
72             try ctx.entryPoint();
73             catch(Error e)
74             {
75                 taskYieldRaise(e.msg);
76             }
77         }
78         coroutineExit();
79     }
80 
81     ~this()
82     {
83         if(this.isValid)
84             this.dispose();
85     }
86 
87     void resume()
88     {
89         assert(this.isValid, "This task is in an invalid state.");
90         assert(!this.hasError, "This task has errored. Used `.error` to see what went wrong.");
91         this._state = TaskState.running;
92         this._context.yieldedWithValue = false;
93         final switch(this._coroutine.state) with(CoroutineState)
94         {
95             case start: coroutineStart(this._coroutine); break;
96             case running: assert(false, "This task is already running.");
97             case suspended: coroutineResume(this._coroutine); break;
98             case end: assert(false, "This task has finished.");
99         }
100 
101         if(this._context.error != BcError.init)
102         {
103             // displayError(this._context.error); // Not ideal, but it's fine until there's a logging system up
104             this._state = TaskState.errored;
105         }
106         else if(this._coroutine.state == CoroutineState.suspended)
107             this._state = TaskState.yielded;
108         else if(this._coroutine.state == CoroutineState.end)
109             this._state = TaskState.done;
110     }
111 
112     void dispose()
113     {
114         assert(this.isValid, "This task is in an invalid state.");
115         this._state = TaskState.uninit;
116         coroutineDestroy(this._coroutine);
117         coroutineDestroyStack(this._stack);
118         this._coroutine = null;
119     }
120 
121     ref T valueAs(alias T)()
122     {
123         assert(this.hasValue);
124         return *this._context.taskYieldValue.ptrUnsafeAs!T;
125     }
126 
127     @property @safe
128     TaskState state() const
129     {
130         return this._state;
131     }
132 
133     @property @safe
134     bool isValid() const pure
135     {
136         return this._coroutine !is null;
137     }
138 
139     @property
140     BcError error()
141     {
142         assert(this.hasError, "This task hasn't errored, there's no reason for this to be called.");
143         return this._context.error;
144     }
145 
146     @property @safe
147     bool hasError() const pure
148     {
149         assert(this.isValid);
150         return this._state == TaskState.errored;
151     }
152 
153     @property @safe
154     bool hasYielded() const pure
155     {
156         assert(this.isValid);
157         return this._state == TaskState.yielded;
158     }
159 
160     @property @safe
161     bool hasEnded() const pure
162     {
163         assert(this.isValid);
164         return this._state == TaskState.done || this.hasError;
165     }
166 
167     @property @safe
168     bool hasValue() const pure
169     {
170         assert(this.isValid);
171         return this._state == TaskState.yielded && this._context.yieldedWithValue;
172     }
173 }
174 
175 void taskRun(T)(ref return Task task, CoroutineFunc func, auto ref T context = null)
176 {
177     import libd.memory : emplaceCtor;
178     emplaceCtor(task, func, context);
179     task.resume();
180 }
181 
182 @nogc nothrow
183 void taskYield()
184 {
185     coroutineYield();
186 }
187 
188 @nogc nothrow
189 void taskYieldRaise(BcError error)
190 {
191     auto ctx = cast(TaskContext*)coroutineGetContext();
192     assert(ctx !is null, "This was not called during a task. Tasks are a more focused layer placed on top of coroutines.");
193     ctx.error = error;
194     taskYield();
195 }
196 
197 void taskYieldRaise(string File = __FILE_FULL_PATH__, string Function = __PRETTY_FUNCTION__, string Module = __MODULE__, size_t Line = __LINE__)(
198     bcstring message,
199     int errorCode = 0
200 )
201 {
202     taskYieldRaise(raise!(File, Function, Module, Line)(message, errorCode));
203 }
204 
205 void taskYieldValue(T)(auto ref T value)
206 {
207     auto ctx = cast(TaskContext*)coroutineGetContext();
208     assert(ctx !is null, "This was not called during a task. Tasks are a more focused layer placed on top of coroutines.");
209     ctx.taskYieldValue.setByForce(value);
210     ctx.yieldedWithValue = true;
211     taskYield();
212 }
213 
214 void taskAccessContext(alias T, alias Func)()
215 {
216     auto ctx = cast(TaskContext*)coroutineGetContext();
217     assert(ctx !is null, "This was not called during a task. Tasks are a more focused layer placed on top of coroutines.");
218     ctx.userContext.access!T((scope ref T value) { Func(value); });
219 }
220 
221 @("task - basic")
222 unittest
223 {
224     Task task;
225     taskRun(task, (){
226         taskYield();
227     });
228 
229     assert(task.isValid);
230     assert(task.hasYielded);
231     task.resume();
232     assert(task.hasEnded);
233 }
234 
235 @("task - context")
236 unittest
237 {
238     Task task;
239     int num;
240     taskRun(task, (){
241         taskAccessContext!(int*, (scope ref ptr){
242             (*ptr)++;
243         });
244         taskYield();
245         taskAccessContext!(int*, (scope ref ptr){
246             (*ptr)++;
247         });
248     }, &num);
249 
250     assert(num == 1);
251     task.resume();
252     assert(num == 2);
253 }
254 
255 @("task - error")
256 unittest
257 {
258     Task task;
259     taskRun(task, (){ taskYieldRaise("error"); });
260     assert(task.hasEnded && task.hasError);
261     assert(task.error.message == "error");
262 }
263 
264 @("task - value")
265 unittest
266 {
267     Task task;
268     taskRun(task, (){
269         taskYieldValue(1);
270         taskYieldValue("string");
271     });
272     assert(task.hasYielded && task.hasValue);
273     assert(task.valueAs!int == 1);
274     task.resume();
275     assert(task.hasYielded && task.hasValue);
276     assert(task.valueAs!string == "string");
277     task.resume();
278     assert(task.hasEnded);
279 }
280 
281 @("task - move support")
282 unittest
283 {
284     import libd.memory : move;
285 
286     Task task;
287     Task moved;
288     int num = 200;
289     taskRun(task, (){
290         taskYield();
291         taskAccessContext!(int*, (scope ref ptr){
292             assert(*ptr == 200);
293             *ptr *= 2;
294         });
295     }, &num);
296 
297     move(task, moved);
298     moved.resume();
299     assert(moved.hasEnded);
300     assert(num == 400);
301 }