coroutines using minicoro

coroutines using minicoro
Photo by 愚木混株 cdd20 / Unsplash

Let's approach coroutines as something we utilise rather than get in the language. Using the minicoro library and the ability to use C code directly from STZ, let's see what it'll take to utilise.

Starting with the basics - let's create a coroutine, call it, then call it again.

minicoro :: using: 'minicoro'.

[self coro_entry: coro]
 [self, coro: &minicoro mco_coro -> ø |
  "coroutine step 1" copy> stdout.
  minicoro mco_yield: coro.
  "coroutine step 2" copy> stdout].

[self main]
 [co := &minicoro mco_coro.
  desc := minicoro mco_desc_init: coro_entry, 0.
  res := minicoro mco_create: &co, &desc.
  --- [
    res := minicoro mco_destroy: co.
    logger assert: res == minicoro MCO_SUCCESS].
  
  logger assert: res == MCO_SUCCESS.
  logger assert: (minicoro mcostatus: co) == minicoro MCO_SUSPENDED.
  
  res := c mco_resume: co. // print out coroutine step 1
  logger assert: res == MCO_SUCCESS.
  logger assert: (minicoro mcostatus: co) == minicoro MCO_SUSPENDED.

  res := minicoro mco_resume: co. // print out coroutine step 2
  logger assert: res == MCO_SUCCESS.
  logger assert: (minicoro mcostatus: co) == minicoro MCO_DEAD]

Immediately we can see 'result' codes that want to be asserted. It'd be better if we used chaining for this instead. We can also hope to make a better 'wrapper' for minicoro, so let's also assume that we've done that.

minicoro :: import: 'minicoro'.

[self coro_entry: coroutine]
 [self, &minicoro Coroutine -> ø |
  stdout print: 'coroutine step 1'.
  coroutine yield.
  stdout print: 'coroutine step 2'].

[self main]
 [coroutine: &minicoro Coroutine.
  [coroutine := minicoro create: &coro_entry] --- [coroutine destroy]
   assert> [coroutine status == SUSPENDED]

   // print out coroutine step 1
   then> [coroutine resume]
   assert> [coroutine status == SUSPENDED]

   // print out coroutine step 2
   then> [coroutine resume]
    
   assert> [coroutine status == DEAD]
   else> panic]

With this approach we can spin up new coroutines whenever we want and yield them. We don't have some kind of universal syntax anymore. This makes it all 'real' in that this compiles directly to the minicoro library. We could therefore look at making a runloop that manages coroutines for us in a co-operative way.

The main use case for that is files and networking. There is a function in minicoro to get the currently running coroutine which would make it possible to have 'yield' as a keyword, or to use the return idea we had previously to yield. If the method is called from outside of a coroutine it'd panic because there'd be no way to yield. There's no way for the compiler to know you're doing that.

Then it's a question of how do we create a coroutine in the first place? either with the low level API of create: or create:stack-size: but if we're going to borrow more pages from Smalltalk-80 we could simply add #fork.

minicoro :: import: 'minicoro'.

[self coro_entry]
 [self, &minicoro Coroutine of: ø -> yield: coroutine |
  stdout print: 'coroutine step 1'.
  yield ø.
  stdout print: 'coroutine step 2'].

[self main]
 [coroutine: &minicoro Coroutine of: ø.
  [coroutine := [self coro_entry] fork --- [coroutine destroy]
   assert> [coroutine status == SUSPENDED]

   // print out coroutine step 1
   then> [coroutine resume]
   assert> [coroutine status == SUSPENDED]

   // print out coroutine step 2
   then> [coroutine resume]
    
   assert> [coroutine status == DEAD]
   else> panic]

We've gained a new superpower - the ability to return values from the coroutine yield. Let's give that a try:

minicoro :: import: 'minicoro'.

[self coro_entry]
 [self -> yield: &minicoro Coroutine of: Integer |
  yield 1.
  yield 2].

[self main]
 [coroutine: &minicoro Coroutine of: Integer.
  result: Integer.
  [coroutine := [self coro_entry] fork --- [coroutine destroy]
   assert> [coroutine status == SUSPENDED]

   // print out coroutine step 1
   then> [result := coroutine resume]
   then> [stdout print: `coroutine step ~[result]`]
   assert> [coroutine status == SUSPENDED]

   // print out coroutine step 2
   then> [result := coroutine resume]
   then> [stdout print: `coroutine step ~[result]`]
    
   assert> [coroutine status == DEAD]
   else> panic]

To use coroutines this way requires coroutines that don't need to return. minicoro is designed to return. To use minicoro for a runloop means the 'yield' keyword only works for that main runloop and nowhere else.

The next time we explore coroutines we'll have to try out Tina which will allow us to fully replace the current execution and move on to the next waiting coroutine without the need for a runloop. That will give us a general purpose 'process' model for the entire language - one that isn't "fixed" to the language but merely a library we call.