Coroutine scope

A coroutine scope consists of two coroutines, a producer and a consumer, running synchronously. Coroutines are always created in producer-consumer pairs. The producer coroutine executes an output scope. It outputs data into its #current-output, which then feeds it to the consumer through its #current-input. The consumer executes an input scope. OmniMark alternates execution between the producer and consumer depending on the data flow between them.

The following actions can be used to create a new coroutine scope:

In all the cases listed above, consumer and producer are both defined by the user. There are simpler cases, however, when OmniMark pairs a user-defined producer with a primitive consumer built into OmniMark, or vice versa. For example, set file "output.txt" to can be applied to a string source function call. The file consumer in this example is built into the language. The opposite example would be applying set ... to file "input.txt" with a string sink function call on the left-hand side of the set action.

OmniMark guarantees that the two coroutines in a producer-consumer pair always start and terminate together. If the consumer coroutine terminates first, the producer will be forcibly terminated and only its always clauses will run. If the producer terminates before the consumer, the consumer receives value-end in its #current-input and runs until it terminates.

In OmniMark, every coroutine function call is scoped in this way. The coroutines always complete their execution before the caller proceeds with its own. This greatly simplifies reasoning about OmniMark programs:

  • The caller does not need to worry that the coroutine called is still running and producing side-effects, nor that it was left suspended in the middle of its execution, nor that it was terminated prematurely without getting a chance to clean up.
  • Coroutines know that their caller will wait until they complete their execution.
  • A coroutine can throw to its caller, just like an ordinary function.
  • Coroutines inherit the current groups, domain-bound global shelves, and parsing state from their caller.

As any other scope in OmniMark, coroutine scopes can nest. A producer coroutine, for example, can invoke another producer-consumer coroutine pair and delegate its work to them. Furthermore, it can pass its #current-output, a reference to its consumer sibling, to either or both of its child coroutines. A chain of coroutines can be built this way.

Prerequisite Concepts