COROUTINES AND ASYNC IN MOTH

Design draft: This page is a future-facing design document for language and compiler planning. It is not final user-facing documentation, and the syntax, semantics and implementation details may change before this feature is implemented.

Moth approaches async and concurrency by making control flow explicit and communication safe by default.

Instead of hiding async behind keywords like async / await or complex runtime abstractions, Moth uses a small set of primitives that make data flow, suspension and concurrency visible in the syntax.

At a high level:

The goal is async code that remains readable, predictable and analysable.

Core idea

Async in Moth is built around three concepts:

Concept Syntax Meaning
Channels Channel(Int) Create typed communication endpoints
Send >> Move or pass a value into a channel
Receive << Wait for a value from a channel
Coroutine spawn Start cooperative work inside an async scope
Suspension yield Explicitly give control back to the scheduler
Scope async: Bound the lifetime of spawned coroutines

The intended model is:

> Async is structured control flow plus safe message passing.

Channels

Coroutines should not share mutable state by default.

Instead, they communicate by sending values through typed channels.

A channel is created with Channel, which takes a carried value type and returns two endpoints:


    reader, writer = Channel(Int)
    

In this example:

You can think of this as a one-way pipe for values of a specific type.

Sending values

Values are sent into a channel with >>.


    value >> writer
    

The value flows to the right into the channel.

Sending may suspend the current coroutine if the receiver is not ready.

This is an intentional visible suspension point.

Receiving values

Values are received from a channel with <<.


    value << reader
    

The value flows left out of the channel and is assigned to value.

Receiving may suspend the current coroutine until a value is available.

Like sending, this is a visible suspension point.

Coroutines

A coroutine is a function that can suspend and resume execution.

Coroutines are started with spawn and must run inside an async scope.

Example coroutine functions:


    producer |writer ~Channel[Int]|:
        loop 0 to 10 |i|:
            i >> writer
            yield
        ;
    ;

    consumer |reader ~Channel[Int]|:
        loop:
            value << reader
            io.line(value)
        ;
    ;
    

In this example:

Yield

yield explicitly suspends the current coroutine and allows other coroutines to run.


    yield
    

This is a cooperative pause point.

It does not block the whole program. It tells the scheduler that the current coroutine is ready to let something else run.

Example:


    loop 0 to 1_000_000 |i|:
        i >> writer

        if i % 128 is 0:
            yield
        ;
    ;
    

Here, yield prevents one long-running coroutine from starving others.

Suspension is always visible in the code.

There are no hidden pause points.

Structured async scopes

Coroutines run inside an async scope.


    async:
        ...
    ;
    

An async scope means:

Example:


    async:
        reader, writer = Channel(Int)

        spawn producer(writer)
        spawn consumer(reader)

        loop:
            yield
        ;
    ;
    

This makes concurrency structured and bounded.

You can see where async work starts, where it may suspend, and where it must be contained.

Why not viral async types?

Moth should avoid making async infect every function type by default.

The draft model does not rely on a special Future[T] return type everywhere, and it does not require every caller to become async just because one operation may suspend.

Instead:

The goal is not to hide async.

The goal is to make async local and visible.

Safety model

The async model should preserve Moth's normal safety rules.

Async code should not allow:

Channels are the main communication primitive because they give the compiler a clear place to reason about ownership, borrowing and transfer.

Mutable shared state may be possible later through specific safe abstractions, but it should not be the default async model.

Full example


    producer |writer ~Channel(Int)|:
        loop 0 to 10 |i|:
            i >> writer
            yield
        ;
    ;

    consumer |reader ~Channel(Int)|:
        loop:
            value << reader
            io.line(value)
        ;
    ;

    async:
        reader, writer = Channel(Int)

        spawn producer(writer)
        spawn consumer(reader)

        loop:
            yield
        ;
    ;
    

This example has three visible forms of async behaviour:

Compiler lowering

The exact lowering strategy is not final.

A future compiler implementation may lower coroutines into state machines.

Conceptually, each coroutine needs to preserve:

This fits Moth's broader compiler direction:

Async should be represented semantically before backend-specific lowering.

Backend considerations

This design depends on backend capabilities.

Important open questions include:

The user-facing syntax should remain stable even if backend strategies differ.

Deferred design questions

Several details should be deliberately deferred until the basic model is proven:

These should be designed as extensions, not packed into the first implementation.

Current status

This design is not implemented yet.

It is intended as a future reference for language design and compiler implementation planning.

The first implementation should likely focus on:

The important part is to keep the source model explicit and structured.