Concurrency
Yona has no async or await keywords. I/O and other asynchronous operations
return promises internally; the compiler inserts an await automatically at the
first point where the value is actually used. Your code reads as if it were
synchronous, and independent work runs in parallel without any annotation.
This page describes the user-level model. For the runtime machinery — io_uring, the thread pool, task groups, cancellation — see Concurrency internals.
Independent let bindings run in parallel
Section titled “Independent let bindings run in parallel”When a let expression has several bindings that do not depend on each other,
every asynchronous right-hand side is submitted before any of them is
awaited:
import readFile from Std\File inlet a = readFile "foo.txt", b = readFile "bar.txt", c = readFile "baz.txt"in a ++ b ++ cAll three reads start immediately; the first await happens at the ++ in the
body, where the string values are needed. Elapsed time is approximately the
maximum of the three read times, not their sum.
The ordering guarantee is precise:
- Binding right-hand sides are submitted in source order, but the program does not wait for one to complete before submitting the next.
- A binding that mentions an earlier binding’s name depends on its value, so the
dependency is awaited first.
let a = readFile p, b = process a in bruns sequentially becausebneedsa. - The body’s first use of a bound value awaits it. Unused promise-valued
bindings are still awaited before the
letscope exits, so no work leaks past the scope.
Contrast with JavaScript-style await
Section titled “Contrast with JavaScript-style await”In JavaScript, await is explicit and each one is a sequencing point:
const a = await readFile("foo.txt");const b = await readFile("bar.txt"); // starts only after a completes!Getting parallelism back requires restructuring into Promise.all. In Yona the
parallel version is the naive version:
let a = readFile "foo.txt", b = readFile "bar.txt"in a ++ bThere are no “colored” functions: an effectful function is called exactly like a pure one, and callers never change shape when a function becomes asynchronous.
Auto-await at use sites
Section titled “Auto-await at use sites”The type system tracks asynchronous values as Promise<T> internally. When a
Promise<T> appears where a T is expected — an operator operand, a function
argument, a condition — the compiler records a coercion and the runtime awaits
at that point:
import readFile from Std\File, split from Std\String inlet content = readFile "data.txt" in # content: Promise<String> internallylet lines = split "\n" content in # first use — awaited herelength lines # lines is a plain Seq, no awaitYou never see the Promise type in ordinary code and never write an await.
Implementation note. Two backends serve these promises. Kernel I/O (files, sockets) is submitted to io_uring on Linux (IOCP on Windows, kqueue on macOS) and returns a submission ID immediately. CPU-bound or blocking operations run on a work-stealing thread pool. The await coercion picks the matching completion call; when io_uring is unavailable (some containers), the runtime falls back to blocking I/O transparently.
Error propagation and cancellation
Section titled “Error propagation and cancellation”A multi-binding let forms an implicit task group. If one binding raises,
its siblings are cancelled — queued thread-pool tasks are skipped and in-flight
kernel I/O is cancelled — and the error propagates to the caller after the group
has quiesced:
let a = readFile "exists.txt", b = readFile "missing.txt" # raisesin a ++ b# The read of "exists.txt" is cancelled; the error propagates.No exception escapes while sibling tasks are still running, and no task outlives
the let scope that created it. This is structured concurrency without any
scope syntax.
do for guaranteed sequential effects
Section titled “do for guaranteed sequential effects”Expressions in a do block execute strictly top to bottom; the last expression
is the block’s value. Use do when ordering itself is the point — two or more
writes, protocol steps, anything where interleaving would be wrong. A single
call does not need do.
import println from Std\Io indo println "first" println "second"end# first# seconddo blocks support intermediate bindings with name = expr, which also execute
in order:
do fd = tcpConnect "localhost" 8080 send fd "hello" response = recv fd 4096 close fd responseendRule of thumb: let for values (the compiler may parallelize independent
bindings), do for effects (the compiler must not reorder). A do block never
runs its steps concurrently, even when they look independent.
with for scoped resources
Section titled “with for scoped resources”with name = resource in body binds a resource for the extent of body and
releases it deterministically when the body completes:
with handle = tcpConnect "localhost" 8080 in send handle "hello"# handle is closed here after the body completesThe resource’s type must implement the Closeable trait; this is checked at
compile time, and a type without a Closeable instance is a compile error (not
a warning). The built-in Closeable Int instance covers file descriptors and
sockets; Closeable FileHandle covers binary file handles. Release order for
nested with scopes is innermost first:
with server = tcpListen "0.0.0.0" 9000 inwith client = tcpAccept server in recv client 1024# client closed first, then serverCurrent limitation. Release runs when the body completes normally; if an
exception propagates out of the body, close is not currently invoked on the
unwind path. Combine with and try/catch inside the body when you must
handle failures before the scope exits.
Parallel comprehensions
Section titled “Parallel comprehensions”[| expr for var = source ] evaluates the body for every element
concurrently, one thread-pool task per element, and collects results in
source order:
[| x * 2 for x = [1, 2, 3, 4, 5] ]# => [2, 4, 6, 8, 10]
[| httpGet url for url = urls ] # all requests in flight at onceThe tasks form a task group with the same guarantees as a multi-binding let:
if any element’s task raises, the remaining tasks are cancelled and the error
propagates. Result order is always the source order regardless of completion
order.
Ordinary comprehensions [ expr for var = source ] remain sequential; the [|
opener is the only difference.
extern async
Section titled “extern async”Foreign C functions can join the transparent-async model. The async modifier
submits the call to the thread pool and returns a promise immediately, with the
usual auto-await at use sites:
extern async slowCompute : Int -> Int inlet a = slowCompute 40, b = slowCompute 2in a + b# both C calls run concurrently; total ≈ max, not sumStandard-library I/O is declared the same way in module interfaces, which is why
Std\File, Std\Net, and Std\Process calls parallelize with no user action:
import exec from Std\Process inlet build = exec "make" ["build"], test = exec "make" ["test"], lint = exec "make" ["lint"]in (build, test, lint)# all three subprocesses run in parallelBeyond the basics
Section titled “Beyond the basics”Transparent async covers independent operations with results. For pipelines,
work queues, and actor-style tasks, Std\Channel provides bounded channels and
Std\Task provides spawn; Std\Parallel provides pmap and pfor. These
build on the same runtime and integrate with task-group cancellation. See
Concurrency internals for channels, deadlock detection,
and the scheduler, and the specification for the
formal semantics.