select waits across completion sources and runs the body belonging to the
first ready source.
left := spawn { fetch_left() }
right := spawn { fetch_right() }
mut chosen := 0
select {
value := left => chosen = value
value := right => chosen = value
}select is a statement and returns void. Arm result types and arm body types
may differ.
Source expressions are evaluated once before the runtime wait. Bind complex sources to named tasks or streams first when their creation order or later cleanup matters.
Arm structure
Each arm has a binding pattern, :=, an awaitable source, =>, and a body:
pattern := source => bodyThe source implements the compiler’s structural selectable protocol through
raw_handle() and await(). Built-in Task[T] and Stream[T] satisfy it.
For a task, selection consumes and reaps the winning task. For a stream, it receives the next value while keeping the stream usable for later selections.
The binding pattern is checked against the selected source’s produced type.
Arm bodies use ordinary statement control flow and can return, break, raise
an error, or update enclosing mutable state where those operations are valid.
Do not place the same single-consumer task in more than one arm. Each arm would refer to the same result and reaping obligation.
Default arms
A default arm makes selection non-blocking:
select {
value := task => consume(value)
default => do_other_work()
}The runtime scans once. It runs default if no source is ready rather than
registering waiters.
default is useful for a poll-and-work loop, but a tight loop can starve other
work if it never reaches another safepoint:
select {
message := inbox => handle(message)
default => perform_bounded_work()
}Keep the default body bounded or call something that allows cooperative progress.
Losing sources
Selection leaves non-winning tasks running and owned by the caller. It removes the temporary wait registrations but does not cancel those tasks.
This differs from Race, which creates its own arm tasks and cancels losers.
After selection, explicitly account for every losing task:
left := spawn { fetch_left() }
right := spawn { fetch_right() }
mut left_won := false
select {
value := left => {
left_won = true
use(value)
}
value := right => use(value)
}
if left_won {
right.cancel()
} else {
left.cancel()
}The winner must not be awaited or canceled again.
Streams
Streams make long-lived multiplexing possible:
mut running := true
for running {
select {
event := events => consume(event)
signal := shutdown => running = false
}
}A closed empty stream wakes selection. The built-in selectable adapter
materializes a value for that wake, so application protocols should carry an
explicit end marker or separately test is_closed() instead of treating every
selected stream value as evidence of a producer item.
Repeated stream selection consumes at most one buffered item per winning arm. The stream itself remains available for the next loop iteration.
Failures
If awaiting a winning source is fallible, an unhandled failure propagates
through the surrounding function’s error channel. A postfix catch on the arm
body can recover or map it using ordinary catch
semantics.
Fairness
Selection is scheduling-dependent. If several sources are already ready, the runtime’s scan order may select an earlier arm. Do not write correctness logic that assumes statistically fair winners.
Empty selections
select {} is accepted as a void no-op. It does not suspend and introduces no
effects. Prefer omitting it; an empty selection usually means dynamic arm
construction belongs in a higher-level abstraction.
Effects
The runtime behavior is “wait and consume one winner,” with no loser
cancellation. The current semantic checker conservatively records Spawn,
Suspend, and Cancel for non-empty selections, including selections with a
non-blocking default arm. Treat that row as an analysis over-approximation,
not runtime behavior.