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.
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.
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:
writer is the sending endpointreader is the receiving endpointIntYou can think of this as a one-way pipe for values of a specific type.
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.
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.
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:
producer sends integers into the channelconsumer receives and prints themyield 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.
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.
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.
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.
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:
spawn starts concurrent work>> and << communicate through the channelyield explicitly gives the scheduler a chance to run other workThe 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.
This design depends on backend capabilities.
Important open questions include:
The user-facing syntax should remain stable even if backend strategies differ.
Several details should be deliberately deferred until the basic model is proven:
Channel[Int] remains the final channel type syntaxThese should be designed as extensions, not packed into the first implementation.
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.