Skip to content

Concurrency

Await = "await" Unary .
AsyncLet = "async" "let" identifier [ ":" Type ] "=" Expr .
Yield = "yield" Expr .
Spawn = "spawn" Block . // expression → Job
TaskFunc = "task" "func" identifier "(" [ ParamList ] ")" [ ReturnType ] Block .
Select = "select" "{" { SelectArm } "}" .
SelectArm = ".recv" "(" identifier "," Expr ")" Block
| ".send" "(" Expr "," Expr ")" Block
| ".timeout" "(" Expr ")" Block
| "default" Block .

chan<T>(n) is a construction expression yielding Chan<T>. await foreach … in … consumes an async stream. spawn, chan, await are reserved; task and yield are contextual.

A function is asynchronous iff its body contains await — there is no async keyword, and asyncness does not propagate up the call chain. The declared return type is the unwrapped value; the awaitable is generated. The return type selects the builder (“semi-coloring”):

Declared returnCLR returnBuilder
bare T / ValueTask<T>ValueTask<T> (default)AsyncValueTaskMethodBuilder<T>
-> Task<T> / -> TaskTask<T> / TaskAsyncTaskMethodBuilder<T>
-> void (explicit)void (async-void)AsyncVoidMethodBuilder
-> IAsyncEnumerable<T> + yieldIAsyncEnumerable<T>channel-backed

The body is identical across the first three; only the wrapper changes, and the call site never does. Explicit -> void is async-void (event handlers) and carries the unobserved-exception caveat.

A function declared -> IAsyncEnumerable<T> whose body uses yield is an async stream; it may interleave yield e (produce) and await (suspend), and is consumed with await foreach. The lowering is channel-backed (a capacity-1 Chan<T> plus a producer task): it gives backpressure, cancellation on early consumer exit, and producer-exception propagation, and runs one element ahead (not a pure lazy pull). A yield outside such a function is ES2131.

async let name = init starts init concurrently at the declaration; the first textual reference awaits it (Swift-style fan-out, written against the unwrapped type). The initializer shall be a call or awaitable (ES3005); a sync user-function initializer is auto-wrapped in Task.Run with ES3004. Lowering happens before either backend sees the tree.

spawn { … } runs a block as a job, yielding a Job. task func f() makes the call site a spawn: calling f() runs the body as a task and returns Job / Job<T>. A function literal inside a task func body shall not capture a var from the surrounding scope (ES2130); thread shared state through chan<T> parameters. let captures are permitted.

chan<T>(n) constructs a bounded channel (unbounded if n omitted), backed by System.Threading.Channels. for v in ch drains until the channel completes. select waits on multiple channel operations: a ready .recv/.send arm fires (fairness shuffle across ready arms); default is held back on the non-blocking pass so a ready operation always wins; without default, select blocks until an operation completes or a .timeout elapses. Inside a spawn body the job’s cancellation token is threaded into select and channel iteration, so a cancelled task unwinds in bounded time.

TaskScope owns child tasks and channels, propagates cancellation in and out, and does not exit until every child completes; a child’s first exception cancels its siblings and is rethrown on exit.

TypeKey members
JobCancel() · Wait() · WaitAsync() -> ValueTask · AsTask()
Job<T>Wait() -> T · WaitAsync() -> ValueTask<T> · AsTask() · Cancel()
Chan<T>Send · SendAsync · ReceiveAsync · TrySend · TryReceive · Complete/Close · for..in / await foreach
TaskScopeRunAsync(...) · Spawn(ct => …) · Token · Chan<T>(n) (auto-completed) · Defer(() => …)

Prefer scope.Chan<T>(n) for auto-completion on scope exit. The language-level spawn { } currently lowers to Job.Spawn; a scope-aware lowering is future work.