Skip to content

Functions

Functions are declared with the return type first, then the function name, optional generic parameters, parameters, and body.

Int add(Int left, Int right) {
return left + right;
}

Every function has a return type. Use () for Unit:

() log_done() {
print("done");
return ();
}

Parameters are written as Type name. Defaulted parameters may appear anywhere in the parameter list. Default values currently use literals and are checked against the parameter’s expected type:

Int scale(Int factor = 2, Int value) {
return value * factor;
}
Int result = scale(2, 21);
Int defaulted = scale(value: 21);

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:

Int value = scale(2, 21);
Int named = scale(factor: 2, value: 21);
Int reordered = scale(value: 21, factor: 2);
Int defaulted = scale(value: 21);
Int missing = scale(21); // error: 21 binds factor, leaving value missing.

Positional arguments cannot appear after named arguments:

Int bad = scale(value: 21, 2); // error: positional argument after a named argument.

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.

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.

Int f(Int value) {
return value;
}
Int f(Int value, Int extra = 1) {
return value + extra;
}
Int a = f(10); // error: both overloads can match.

Use different names or non-overlapping parameter lists instead.

Local Declarations, Value Blocks, and Returns

Section titled “Local Declarations, Value Blocks, and Returns”
Int abs(Int value) {
if value < 0 {
return -value;
} else {
return value;
}
}

return is a statement. An explicit Unit return still writes the Unit value:

() done() {
return ();
}

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 Unit value:

Int value = {
Int base = 40;
base + 2
}
() unit_value = {
Int ignored = 1;
}

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:

Int add(Int left, Int right) {
return left + right;
}
Int add_tail(Int left, Int right) {
left + right
}

Use return for early returns, branch exits, or explicit Unit returns.

Use tuples for multiple returned values:

(Int, Int) split(Int value) {
return (value, value + 1);
}
Int main() {
(_ left, _ right) = split(41);
return left + right;
}

(T) is equivalent to T; a one-element tuple is not a distinct runtime shape.

Generic parameters follow the function name:

T id<T>(T value) {
return value;
}
@where(T: Numeric)
T add<T>(T left, T right) {
return left + right;
}

@where(...) is a leading annotation before the generic declaration.

Functions inside a type are associated functions unless their first parameter is a receiver. Write self for a readonly Self& receiver:

struct User {
Int id;
User zero() {
return User(id: 0)
}
Int value(self) {
return self.id;
}
}
User user = User(id: 42)
Int a = user.value();
User z = User.zero();

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:

Int a = user.value();
Int b = User.value(user$.ref());

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 matching RawFn.
  • 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.

Bool less(Int left, Int right) {
return left < right;
}
RawFn<Bool, Int, Int> raw_compare = less;
Fn<Bool, Int, Int> compare = Fn(raw_compare);
Bool ok = compare(1, 2);

Anonymous functions use the expected type from their context:

Fn<Int, Int> add_base(Int base) {
Fn<Int, Int> add = { value => value + base };
return add; // error: the moved stack Fn cannot let its captured environment escape.
}

A closure with no parameters starts with { =>. An explicit capture list comes before the parameters, as in { [snapshot = value] delta => snapshot + delta }.

When the final parameter is Fn or RawFn, its argument can be written as a trailing closure:

() run(Fn<()> work) {
work();
}
run { print("done"); };
map(values) { value => transform(value) };

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 new to create an owned closure:

Fn<Int>^ make_answer(Int value) {
return new { [_ snapshot = value] => snapshot };
}
Int main() {
Fn<Int>^ answer = make_answer(42);
return answer() - 42;
}

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:

Int call_optional(Fn<Int>^? maybe) {
guard maybe is .some(ref _ answer_ref) else {
return 0;
}
Fn<Int>& answer = answer_ref$.ref();
return answer();
}

An unbound instance method includes the receiver reference as its first parameter and is represented as a RawFn:

RawFn<Int, User&> get_value = User.value;
Int value = get_value(user$.ref());

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:

RawFn<unsafe Int, Int>[]& unsafe_callbacks;

Calling a function with the unsafe effect requires an explicit effect context:

Int value = unsafe {
unsafe_callback(1)
};

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 Int load_page() {
1
}
async Int render() {
load_page() + 1
}

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:

async Int load_both() {
Task<Int> left = Task { load_left() };
Task<Int> right = Task { load_right() };
left.await() + right.await()
}

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<Int>^ start_load() {
new Task(domain: worker) { load_page() }
}
struct Request {
Task<Int>^ task;
}

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.

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 a const value whose type implements Domain. Distinct const bindings are distinct Domains even when they have the same type and value:

struct WorkerDomain: Domain<kind = .concurrent> {}
const WorkerDomain worker = WorkerDomain();
async [worker] Int load_on_worker() {
1
}

Task(domain: worker) { ... } starts a Task on that Domain; Task { ... } inherits the current Domain. The removed async { ... } block form is not accepted. In an async function, sync [worker] { ... } is a structured Domain switch: it suspends the current coroutine without blocking a worker thread and resumes on the entering Domain afterward. The outermost sync [worker] in an ordinary function blocks the calling thread until completion.

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.