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
};