coroutines using tina

coroutines using tina
Photo by kaleb tapp / Unsplash

The idea that we can support different kinds of coroutines libraries in STZ makes me happy. So much so I think it's worth abandoning the Smalltalk-80 fork API and providing the libraries as-is, with some nice wrapping to clean them up.

Tina provides us not only with asynchronous coroutines (...basically gosub), but also synchronous (...basically goto). On top of that it has a fiber library for managing coroutine jobs across multiple threads. That might be too much for now but we can always play with it.

The problem with sync coroutines is you cannot use them on WASM. I'm not sure how I feel about that - perhaps WASM needs to change, or we are forced to use async to support fancier APIs even in a web browser.

The web browser doesn't give you regular files or sockets though so it might not even need sync coroutines. That would suggest some kind of abstraction for the platform you're deploying to. That's not so uncommon.

But, cracking on with Tina. Let's try and write a basic sync coroutines in STZ using it. We'll skip the raw C conversion and do the usual idiomatic wrapper.

tina :: using: 'tina'.

// A simple coroutine queue
//

Coroutines :: List of: &tina Coroutine.

[routines terminate: coroutine]
 [routines: &Coroutines, coroutine: &tina Coroutine |
  next := routines next-after-wrap: coroutine.
  coroutine == next then: [panic].
  routines remove: coroutine.
  tina swap: next argument: next, routines.
  panic].

[routines yield: coroutine]
 [routines: &Coroutines, coroutine: &tina Coroutine -> return: ø|
  next := routines next-after-wrap: coroutine.
  coroutine == next then: [return ø].
  tina swap: next argument: next, routines].


// Example Coroutine entry points
//

[self entry-a: this of: routines]
 [self, this: &tina Coroutine, routines: &Coroutines -> ø |
  stdout print-line: 'entry-a 1'.
  routines yield: this.

  stdout print-line: 'entry-a 2'.
  routines terminate: this.
  panic].

[self entry-b: this of: routines]
 [self, this: &tina Coroutine, routines: &Coroutines -> ø |
  stdout print-line: 'entry-b 1'.
  routines yield: this.

  stdout print-line: 'entry-b 2'.
  routines terminate: this.
  panic].

[self main]
 [// create a coroutine to represent the main process
  main-coro: tina Coroutine = tina EMPTY.

  // create entry point coroutines
  a-coro := tina init: [:this :all | self entry-a: this of: all].
  b-coro := tina init: [:this :all | self entry-b: this of: all].

  routines: Coroutines.
  routines add-all: {&main-coro, &a-coro, &b-coro}.

  stdout print-line: 'main-start'.
  routines yield: main-coro.

  stdout print-line: 'main-middle'.
  routines yield: main-coro.
  
  stdout print-line: 'main-done']

First brush it's pretty simple to use. Slightly easier than minicoro. To pass parameters we can provide an argument with swap:argument:. There is no return so there is no return value to be stored anywhere.

If we were writing an IO library we'd pass an argument with an instruction that also contains a place to store a result.

To do non-blocking IO we have to use a threading library of one kind or another. We know it'll be platform dependent. The nice thing about sync coroutines is we don't need a runloop. Each time a coroutine yields it swaps to the next interested coroutine in the list.

It's worth noting that we don't need any kind of built in support for the coroutines for either minicoro or tina. I think I prefer it that way. STZ is meant to be a low level language that builds itself up to a medium to high level language without forcing compromises on the developer. By not building coroutines in to the language, but rather providing them as a way to achieve non-blocking IO, we can swap out how it's done any time it becomes a problem in the future.

We haven't looked at tina's fiber implementation yet. There's trickery in STZ when it comes to keeping our memory safe between OS threads. As such I'll leave that topic to another post some time in the future.