Coroutines

Coroutines are functions that logically are like regular functions, but they can pause and resume their execution. The internal state of a coroutine - arguments, local variables, temporary variables, etc. is saved after pause and restored after resume.

There are following coroutine kinds:

Coroutine-objects are objects of special classes (that are generated by the compiler) which are created by a call of a coroutine-function. A coroutine-object stores coroutine state inside. It’s possible to resume coroutine-object execution via if_coro_advance operator and obtain the result of this resumption. Coroutine objects are not copyable, but destructible (have a destructor) and may be freely moved. Also they are equality-comparable, but each coroutine object is unique and compare result is true only if an object is compared against itself.

if_coro_advance

if_coro_advance operator allows to work with coroutine-objects - resume execution and obtain the result.

This operator does following: checks if passed coroutine-object is not finished yet and if not - resumes its execution. Than it checks again if the coroutine is finished. What it does next depends on the coroutine kind.

In case if it is a generator and it’s not finished yet its result is obtained (that was previously produced by the yield operator) and bound to a specified variable. Than control flow is passed to specified code block.

In case if it is an async function, control flow to the specified code block will be passed only if the async function was finished after current resume (and thus returned a value).

Example:

fn generator SomeGen() : i32;

fn Foo()
{
    auto mut gen= SomeGen();
    if_coro_advance( x : gen )
    {
        // Control flow is passed here if generator is not finished yet. The result of the generator will be placed inside "x".
    }
    else
    {
        // Control flow is passed here if the generator is already finished or is finished after the last resume.
        // "else" branch of "if_coro_advance" operator is optional.
    }
}

A variable defined inside if_coro_advance may have reference and mutability modifiers. Even if the result of a coroutine is not a reference but a variable in if_coro_advance is defined as reference, this result will be bound to this reference, much like it happens in with operator.

Internal representation

A coroutine-function is split into three separate functions by the compiler. The first is an initial coroutine-function, which creates coroutine object, allocates a memory block for it (from the heap) and places arguments inside it. Other two functions are resume and destroy functions, pointers to these functions are placed inside the coroutine-object. In coroutine resuming and destroying these functions are called. Because these functions are called indirectly via a pointer it’s possible to store in one variable of a coroutine type objects produced by calls to different coroutine-functions.

All local variables, references, temporary values, etc. are stored in a memory block allocated for a coroutine-object.

A coroutine object stores inside also a pause point index where executing last time was paused. Resume/destroy functions pass control flow to a code corresponding to this pause point.

Each coroutine has at least two pause points - initial and final. In the initial pause point object resides after the construction. At the final pause point object resides after coroutine finishes.

A coroutine object class contains single (hidden) field inside - a pointer to a memory block. This block contains resume/destroy functions addresses, pause point index, storage for a result and storage for arguments, local variables, temporary values. The contents of this block is not accessible by a programmer, access happens only by the compiler itself.

Mentioned above heap memory allocation may be in some cases optimized-out by the compiler and replaced with a stack allocation if, for example, a coroutine object is created, used and destroyed within single function. But there is no strong guarantee that such optimization happens.

Coroutine object destruction

It’s allowed to destroy a coroutine object when its execution is not finished yet and when it still contains in its memory local variables and arguments. In such case all these variables will be destroyed (destructors will be called) as usual. This destruction is performed via coroutine object destructor, that calls coroutine-specific destruction function.