Functions
Functions are declared with the return type first, then the function name, optional generic parameters, parameters, and body.
Return Types
Section titled “Return Types”Every function has a return type. Use Void when no result is needed; its only value is ():
Parameters
Section titled “Parameters”Parameters are written as Type name. Defaulted parameters may appear anywhere in the parameter
list. A default can be an ordinary expression in the declaration scope and is checked against the
parameter’s expected type. The expression is evaluated at each call site that omits the argument;
passing the argument explicitly does not evaluate it:
Call arguments may be positional or written with name = expr. A positional argument always binds the
earliest unbound parameter; it never skips a default by looking at the argument type. Named arguments
can be reordered and can skip parameters that have defaults:
Positional arguments cannot appear after named arguments:
Named arguments are matched by parameter name, so they may appear in a different order from the function declaration. Omitted default parameters are filled before the call runs.
Defaults and Overloads
Section titled “Defaults and Overloads”If several overloads share the same name, Jiang chooses the one that matches argument count, argument names, and argument types. Default parameters participate in ambiguity checks: if two overloads can both match the same call by arity and types, the declaration is ambiguous.
Use different names or non-overlapping parameter lists instead.
Local Declarations, Value Blocks, and Returns
Section titled “Local Declarations, Value Blocks, and Returns”return is a statement. A Void function may omit its value:
Blocks can be used as value blocks. Statements do not contribute to the block value; the value comes
only from the final tail expression without a semicolon. A block without a tail expression has Void
value:
A function body is also a block, so a tail expression at the end of the body can be the function result. These two forms are equivalent:
Use return for early returns or branch exits.
Multiple Results
Section titled “Multiple Results”Use tuples for multiple returned values:
(T) is equivalent to T; a one-element tuple is not a distinct runtime shape.
Generic Functions
Section titled “Generic Functions”Generic parameters follow the function name:
@where(...) is a leading attribute before the generic declaration.
Named keyword options use the same = spelling: struct [align = 8] and async [domain = ui_domain].
Colons in type constraints and lifetime attributes keep their existing meaning.
Methods
Section titled “Methods”Functions inside a type are associated functions unless their first parameter is a receiver.
Write self for a readonly Self& receiver:
The receiver must be explicit in the parameter list. Ret method(self, ...) uses Self&;
Ret method(Self&! self, ...) requires a unique mutable receiver; and
Ret method(Self self, ...) consumes the receiver by value. Associated functions omit a receiver.
init(self, ...) is a constructor with an initializing self target. It is called through
Type(...) or new Type(...), and is not exposed as a normal function value. deinit(self) is
the cleanup entry point and is not called manually as an ordinary function.
Instance calls can also be written with an explicit receiver argument:
Function Values and Closures
Section titled “Function Values and Closures”Jiang separates C-style function pointers from closures:
RawFn<R, A, B, ...>is a raw function pointer. It has no captured environment and is the type to use for C ABI callbacks.Fn<R, A, B, ...>is a Jiang closure value. It can capture local values and can also wrap a matchingRawFn.FnOnce<R, A, B, ...>is a consuming closure. Calling it moves the closure, which permits moving value captures out of its body.Fn<R, A, B, ...>^is an owned closure handle. Use it when the closure value must outlive the current stack frame.
The first type argument is the return type, followed by parameter types.
Anonymous functions use the expected type from their context:
A closure with no parameters starts with { =>. An explicit capture list comes before the
parameters and selects an existing local: [name] captures by value, while [ref name] and
[ref! name] create shared and unique borrows. A capture list cannot declare, rename, type, or
initialize a variable. Create an ordinary local first when a closure needs a snapshot or expression:
Locals omitted from the list are captured as shared views. Raw pointers are the exception and are
captured by value. Writing outer storage requires an explicit [ref! name] capture.
When the final parameter is Fn or RawFn, its argument can be written as a trailing closure:
In trailing position, { statements } means a zero-parameter closure. A standalone brace body remains
a normal block; write => when a standalone closure has no parameters. A call accepts one trailing
closure, and any earlier omitted parameters must have defaults and be skipped by name.
A stack Fn is movable but not copyable, so ordinary value use moves it by default. It can be passed
to functions that call it immediately, but it cannot be returned or stored somewhere that may
outlive the captured stack values.
Use FnOnce only when the callback must consume a captured value. A FnOnce cannot be called
through FnOnce&, and it cannot be called a second time.
Use new to create an owned closure:
Fn^ can be called directly. If pattern matching or field access gives you a reference to an owned
closure handle, borrow a callable view first:
An owned closure owns its closure object and all by-value captures. Borrowed captures remain borrows:
putting them in Fn^ does not extend their sources. All borrowed captures contribute to one callable
lifetime position, so the owned closure cannot outlive the shortest borrowed capture.
An unbound instance method includes the receiver reference as its first parameter and is represented
as a RawFn:
unsafe can be written before the return type inside RawFn<...> or Fn<...> to mark the context
required to call the value. It describes the outer callable type, not the return value itself:
Calling a function with the unsafe effect requires an explicit effect context:
Async Functions and Tasks
Section titled “Async Functions and Tasks”An async function is a stackless coroutine. Calling it from an async context implicitly suspends
the caller until the result is available, so the call expression has the declared result type:
Async functions, async RawFn values, and async Fn closures use the same call and suspension
model. A closure may capture an environment; its captures still follow the normal ownership,
lifetime, and cross-Domain Sendable rules.
Use Task { ... } to start work eagerly in the current Domain, or
Task(domain = worker) { ... } to choose a Domain explicitly. The result is an address-stable
Task<T>. This direct form is structured and !Movable: it stays in its enclosing scope. It may
also be initialized directly in a statically addressable struct, tuple, or fixed-array field; an
aggregate containing it cannot be moved, passed or returned by value, or captured. Starting multiple
Tasks before awaiting them preserves concurrent progress even when they are awaited in source order:
Use new Task { ... } or new Task(domain = worker) { ... } when the Task owner must independently
move, be returned, be passed to another function, or be stored in a movable field or container. The
result is a movable, non-copyable Task<T>^; moving it transfers only the owner pointer and never
relocates the running Task:
Task<T>^ has the same capture lifetime shape as direct Task<T>; ^ changes storage ownership,
not borrowing. A factory with no borrowed source uses @life(). A returned owner that captures a
borrow instead declares that source with @life(return: source), and a containing field binds the
Task lifetime through the struct’s @region. Because owner destruction does not wait, an owner that
carries capture loans must be consumed with await() or cancel_and_await() before it is dropped.
task.await() waits and consumes the result exactly once. task.cancel() is a synchronous,
idempotent request: it does not wait and does not consume the result, so a later await() remains
valid. task.cancel_and_await() requests cancellation and asynchronously waits for the Task to exit.
Dropping a Task<T>^ owner neither blocks nor implicitly cancels; the runtime reclaims the Task after
both the owner and coroutine have finished with it. Direct Task<T> values remain structured and are
cleaned up before their enclosing scope exits: unfinished children are first cancelled and then
awaited before the remaining locals are destroyed.
Cancellation is cooperative. Suspend and resume boundaries observe requests automatically;
long-running computation that does not suspend should call coroutine.check_cancelled() periodically.
It returns immediately when no request exists and otherwise enters the current coroutine’s cleanup.
If ordinary await() observes a child that ended through cancellation, it also cancels the current
parent; structured cleanup then cancels and waits for unfinished siblings. Use cancel_and_await()
when only the selected Task should stop and the caller should continue.
A Domain effect names where async work runs. Start with the two standard Domains:
main_domain is serial and bound to the program’s startup thread. global_domain uses the shared
concurrent executor. Task(domain = global_domain) { ... } starts a Task there; Task { ... }
inherits the current Domain. The removed async { ... } and sync [domain] { ... } block forms are
not accepted. coroutine.sync(domain) { ... } always requires an explicit Domain. In an async
function it suspends the current coroutine, runs the closure on the target Domain without blocking a
worker thread, and then resumes on the entering Domain. An outermost coroutine.sync(main_domain) in
an ordinary function blocks the calling thread until completion while pumping main-thread work.
Values transferred by value across Domains must preserve their normal move or copy capability and
also satisfy Sendable. A shared T& may cross when T: Sendable, but its ordinary lifetime still
cannot outlive the source owner. A unique T&! does not cross directly. To pass a shared view,
explicitly create one with $.ref(); writes through the original unique reference stay frozen while
that shared reborrow is live. Raw pointers are not automatically Sendable.
Applications with their own queue or event loop can provide an Executor and bind it to a Domain:
The same Domain type supports two lifetime models. A named const binding is a program-wide
execution identity and lazily owns one Executor instance. It is the preferred form for long-lived
shared Domains and has lower scheduling overhead than a runtime Domain.
Create an ordinary Domain value when a page, scene, or session needs an independent execution identity that can be released when that owner is no longer used. Pass a shared reference when selecting it:
Only a named const Domain may appear in an async [domain] effect. A Task accepted by a runtime
Domain can finish after the Domain owner leaves scope; dropping the owner does not implicitly cancel
it. The Domain’s resources are released after its accepted Tasks finish. By contrast,
coroutine.sync(scene_domain$.ref()) is structured: it borrows the Domain only until the closure and
its child coroutines complete, and the returned value does not carry that Domain lifetime.
Choose based on ownership first: use const for shared application-lifetime execution identities,
and an ordinary Domain value for independently owned, finite-lived execution resources. The runtime
form pays modest dynamic-lifetime overhead so that those resources can be reclaimed deterministically.
ExecutorJob is a move-only, one-shot value: after accepting it, an Executor must eventually call
run() exactly once. An Executor may receive concurrent enqueue calls and must synchronize its
own mutable queue state.
Use Atomic<T> for supported lock-free scalar state. The default operations use sequential ordering;
the same operation names accept an explicit MemoryOrder when weaker ordering is required:
Use Mutex<T> for compound shared state. Mutex<T> is address-stable and !Movable; when
T: Sendable, transfer a Mutex<T>^ owner handle across Domains. with_lock provides a unique
mutable reference only for the synchronous callback, so it cannot be held across an await.
Low-level async APIs can suspend explicitly with coroutine.suspend(registration). The registration
callback receives a continuation; call resume(value) on completion and register active cancellation
with on_cancel(handler). A late resume is ignored after cancellation has claimed the operation.
Use RawFn when you need an ABI-level function pointer. Use Fn for normal callback APIs that may
accept capturing closures.