Second Draft: Coroutines

Mai Lapyst mai at lapyst.by
Fri Jan 24 04:33:52 UTC 2025


On Thursday, 23 January 2025 at 23:09:42 UTC, Richard (Rikki) 
Andrew Cattermole wrote:
>
> On 24/01/2025 10:17 AM, Sebastiaan Koppe wrote:
>> On Thursday, 23 January 2025 at 20:37:59 UTC, Richard (Rikki) 
>> Andrew Cattermole wrote:
>>>
>>> On 24/01/2025 9:12 AM, Sebastiaan Koppe wrote:
>>>> Upon yielding a coroutine, say a socket read, you'll want to 
>>>> park the coroutine until the socket read has completed. This 
>>>> requires a signal on completion of the async operation to 
>>>> the execution context to resume the coroutine.
>>>
>>> Right, I handle this as part of my scheduler and worker pool.
>>>
>>> The language has no knowledge, nor need to know any of this 
>>> which is why it is not in the DIP.
>> 
>> Without having a notion on how this might work I can't 
>> reasonably comment on this DIP.
>> 
>>> How scheduling works, can only lead to confusion if it is 
>>> described in a language only proposal (I've had Walter attach 
>>> on to such descriptions in the past and was not helpful).
>> 
>> You don't need to describe how scheduling works, just the 
>> mechanism by which a scheduler gets notified when a coroutine 
>> is ready for resumption.
>> 
>> Rust has a Waker, C++ has the await_suspend function, etc.
>
> Are you wanting this snippet?
>
> ```d
> // if any dependents unblock them and schedule their execution.
> void onComplete(GenericCoroutine);
>
> // Depender depends upon dependency, when dependency has value 
> or completes unblock depender.
> // May need to handle dependency for scheduling.
> void seeDependency(GenericCoroutine dependency, 
> GenericCoroutine depender);
>
> // Reschedule coroutine for execution
> void reschedule(GenericCoroutine);
>
> void execute(COState)(GenericCoroutine us, COState* coState) {
>     if (coState.tag >= 0) {
>         coState.execute();
>
>         coState.waitingOnCoroutine.match{
>             (:None) {};
>
>             (GenericCoroutine dependency) {
>                 seeDependency(dependency, us);
>             };
>
>             // Others? Future's ext.
>         };
>     }
>
>     if (coState.tag < 0)
>         onComplete(us);
>     else
>         reschedule(us);
> }
> ```
>
> Where ``COState`` is the generated struct as per Description -> 
> State heading.
>
> Where ``GenericCoroutine`` is the parent struct to ``Future`` 
> as described by the DIP, that is not templated.
>
> Due to this depending on sumtypes I can't put it in as-is.
>
> Every library will do this a bit differently, but it does give 
> the general idea of it. For example you could return the 
> dependency and have it immediately executed rather than let the 
> scheduler handle it.

First off: nice work on the proposal here; I really like it. 
Would love to try it once it's in an beta stage as it's quite 
promising.
As an individual that implemented their own userspace eventloop 
via fibers, I would love to have another utility in my belt to 
use in the implementation.

The only thing I had a hard time figuring out what you ment by 
"If it causes an error, this error is guaranteed to be wrong in a 
multi-threaded application of it.";
What I think is that you mean that any exception created / 
captured by the coroutine is guranteed to be indeed an execption 
and should be threaded as such.
Correct me if I'm wrong.

Another thing is the visibility of the members of the created 
struct; shouldn't some of them be read-only (aka const for anyone 
outside) or completly be private?
Like `tag`: there should be no situation where an outside entitiy 
should control the state of the coroutine, not even in as a part 
of a library or do I miss something?

> Then `yield` would be a keyword, which in turn breaks code 
> which is known to exist.

Which is the same with `await`; I honestly like the way rust 
solved it: any Future (rust's equivalent to a coroutine type), 
has implicitly the `.await` method, so instead of writing `await 
X`, you have `X.await`. This dosn't break exisiting code as 
`.await` is still perfectly fine an method invocation. When we're 
here to reduce breaking code as much as possible, I strongly 
would go with the `.await` way instead of adding a new keyword.

For yield the only thing I can think of is to introduce a way 
like `Fiber.yield`, maybe `Coro.yield` that gets picked up by any 
dlang edition that understands coroutine and gets rewritten into 
a proper yield while older versions would see a reference to an 
function / field, which can be provided to these editions as a 
symbol with `static assert(false, "...")` to inform them about 
the inproper usage; but that would have the same problems as 
there could well be already such a construct... But if we're 
using an attribute, I like the `@yield` from Quirin's post a lot 
more (and `__yield` seems very clumpsy to me).

> Rust has a Waker, ...
> ...
> ```
> coState.waitingOnCoroutine.match{
>     (:None) {};
>     (GenericCoroutine dependency) {
>         seeDependency(dependency, us);
>     };
>     // Others? Future's ext.
> };
> ```

The waker design seems much more flexible than a dependency 
system. For example, with wakers one could implement asyncronous 
IO by using epoll and invoking the waker when there's date 
available. I'm a bit confused on how that would look in your 
proposal. Sure your executor uses a match on a sumtype to 
determine what's it waiting on, but how does one "register" a 
custom dependency type? Granted, the compiler can scan the code 
and pickup any type thats been waited on as a dependency, but how 
does a executor know how to handle it? Currently, the type must 
be known beforehand from the executor, thus meaning that the 
executor and the IO library must be developed as one, instead of 
being two seperate things that only share a common protocol 
between them. And even when having compiler support for sumtypes, 
when the sumtype is dynamically created, there will be times 
where the sumtypes does not contain all types the executor can 
process, ending up with unreachable branches which could lead to 
compiler warnings or even errors that are cryptic.

While I agree that we should have a notion on how coroutines can 
be put to sleep until an certain event took place, I think 
dependencies aren't a great solution to that. As mentioned would 
a waker API be better suited for this task as it lets executor 
and IO be their own thing instead of trying to forcefully combine 
it into one.


More information about the dip.development mailing list