Second Draft: Coroutines

Mai Lapyst mai at lapyst.by
Fri Jan 24 20:49:05 UTC 2025


On Friday, 24 January 2025 at 06:16:27 UTC, Richard (Rikki) 
Andrew Cattermole wrote:
> "If the compiler generates an error that a normal function 
> would not have, the error is guaranteed to not be a false 
> positive when considering a multithreaded context of a 
> coroutine."

With error you mean an exception? As there are compiler errors 
(as in the compiler refuses to compile something), and execptions 
(i.e. `throw X`). Just makeing sure we're on the same page. If 
so, then I get what you are meaning and should ofc be the case, 
as is not really different as non-multithreaded non-coroutine 
code: any exception thrown shouldn't be a false-positive as long 
as the logic guarding it is not flawed in any form.

>> 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?
>
> You may wish to complete a coroutine early.
>
> Nothing bad should happen if you do this.
>
> If it does, that is likely a compiler bug, or the user did 
> something nasty.

Hmmm, thats indeed a reason for changing `tag`; you wouldn't need 
a cancelation token as the tag is this cancelation token to some 
extend. On that note, we could add a third negative value to 
indicate an coroutine was canceled from an external source or one 
could generally specify that any negative value means canceled 
and libraries can "encode" their own errorcodes into this...

>>> 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.
>
> I don't expect code breakage.
>
> Its a new declaration so I'd be calling for this to only be 
> available in a new edition.

Sadly it will; take for example my own little attempt to build a 
somewhat async framework ontop of fibers: 
https://github.com/Bithero-Agency/ninox.d-async/blob/f5e94af440d09df33f1d0f19557628735b04cf43/source/ninox/async/futures.d#L42-L44 it declares a function `await` for futures; if `await` will become a general keyword, it will have the same problems as if `yield` becomes one: all places where `await` was an identifier before become invalid.

> Worse case scenario we simply won't parse it in a function that 
> isn't a coroutine.

Which could be done also with `yield` tbh. I dont see why `await` 
is allowed to break code and `yield` is not. We could easily make 
both only available in coroutines / `@async` functions.

> I am struggling to see how the waker/poll API from Rust is not 
> a more complicated mechanism for describing a dependency for 
> when to continue.

It's easier, as it describes how an coroutine should be woken up 
by the executor, a dependency system is IMO more complicated 
because you need to differentiate between dependencies whereas 
Wakers serve only one purpose: wakeup a coroutine / Future that 
was pending before to be re-polled / executed.

---

I've read a second time through your DIP and also took a look at 
your implementation and have some more questions:

> opConstructCo

You use this in the DIP to showcase how an coroutine would be 
created, but it's left unclear if this is part of the DIP or not. 
Which is weird because without it the translation
```d
ListenSocket ls = ListenSocket.create((Socket socket) {
	...
});
```
to
```d
ListenSocket ls = ListenSocket.create(
	InstantiableCoroutine!(__generatedName.ReturnType, 
__generatedName.Parameters)
		.opConstructCo!__generatedName);
);
```
would not be possible as the compiler would not know that 
opConstructCo should be invoked here.

Which also has another problem: how do one differentiate between 
asyncronous closures and non-asyncronous closures? Because you 
clearly intend here to use the closure passed to 
`ListenSocket.create` as an coroutine, but it lacks any indicator 
that it is one. Imho it should be written like this:
```d
ListenSocket ls = ListenSocket.create((Socket socket) @async {
	...
});
```

> GenericCoroutine

Whats this type anyway? I understand that `COState` is the state 
of the coroutine, aka the `__generatedName` struct which is 
passed in as a generic parameter and I think the 
`execute(COState)(...)` function is ment to be called through a 
type erased version of it that is somehow generated from each 
COState encountered. But what is `GenericCoroutine` itself? Is it 
your "Task" object that holds not only the state but also the 
type erased version of the execute function for the executor?

> Function calls

I also find no information in the DIP on how function calls 
itself are transformed. What the transformation of a function 
looks like is clear, but what about calling them in a non-async 
function? I would argue that this should be possible and have an 
type that reflects that they're a coroutine as well as the 
returntype, similar to rust's `Future<T>`. This would also proof 
that coroutines are zero-overhead, which I would really like them 
to be in D.

> ```d
> struct AnotherCo {
>     int result() @safe @waitrequired {
>         return 2;
>     }
> }
> 
> int myCo() @async {
>     AnotherCo co = ...;
>     // await co;
>     int v = co.result;
>     return 0;
> }
> ```

How is `AnotherCo` here a coroutine that can be `await`ed on? 
With my current understanding of your proposal, only functions 
and methods are transformed, which means that `AnotherCo.result` 
would be the coroutine, not it's whole parent struct.




More information about the dip.development mailing list