Laurel User Guide

4.12. Coroutines🔗

Laurel models cooperative concurrency with coroutines: procedures that can voluntarily suspend themselves with yield, handing control back to their caller, and can later be resumed with resume. This closely matches Python generators and JavaScript coroutines, where yield suspends execution and next(...) resumes it.

What a coroutine may assume, and must establish, at each suspension is a contract, and is covered under Verification - Continued.

4.12.1. Declaring a coroutine🔗

A resumable procedure (or coroutine) is declared with the coroutine keyword, and its body may contain yield. Two optional channel clauses declare the values that flow across a suspension: yields (x: T) is the outgoing channel (the value handed out at a yield), and resumes (y: U) is the incoming channel (the value sent back in on the next resume). To yield a value, assign it to the yields binding and then yield; yield itself is nullary.

coroutine counter(n: int) yields (x: int)
{
  var i: int := 0;
  while (i < n)
  {
    x := i;   // put the value on the outgoing channel
    yield;    // suspend; the caller sees x
    i := i + 1
  }
};

4.12.2. Driving a coroutine: resume and has_next🔗

A caller spawns a coroutine by calling its name, then advances it one suspension at a time with resume. In expression position, resume(co) evaluates to the value the coroutine just put on its yields channel; resume(co, v) additionally sends v in on the resumes channel. has_next(co) reports whether the coroutine has more steps to run, so a driver loop reads:

procedure drive()
  opaque
{
  var co: counter := counter();
  while (has_next(co))
  {
    resume(co)
  }
};

4.12.3. Planned: async / await🔗

The examples above are supported today. The async/await surface syntax that source languages use is planned, and desugars onto the same coroutine primitives: await g(y) drives g to completion and takes its result. The Python program

# Python
async def fetch_page(cursor):
    await asyncio.sleep(0.1)
    if cursor >= 3:
        return None
    return cursor + 1

async def download_all():
    cursor = 0
    while cursor is not None:
        cursor = await fetch_page(cursor)

is intended to be written with coroutine return values and await as below. This does not compile yet: a coroutine return value and the await sugar are planned (see the Designer Guide's concurrency section). The block is shown to illustrate the mapping, not as working syntax.

coroutine fetch_page(cursor: int): int
{
  yield;                          // models `await asyncio.sleep(0.1)`
  if cursor >= 3 then {
    return -1                     // models `return None`
  } else {
    return cursor + 1
  }
};

coroutine download_all(): int
{
  var cursor: int := 0;
  while (cursor >= 0) {
    cursor := await fetch_page(cursor)   // drives fetch_page to completion
  };
  return cursor
};