A call supplies one value for each parameter in a callable’s static signature. Free functions, methods, static functions, function values, and closures all use the same rules.
struct User {
first: string
last: string
fn display_name(self): string => "${self.first} ${self.last}"
fn anonymous(): User => User { first: "anon", last: "" }
}
fn add(a: int, b: int): int => a + b
fn main(): void {
total := add(20, 22)
name := User { first: "Ada", last: "Lovelace" }.display_name()
fallback := User.anonymous()
empty := List.new[string]()
mapped := [1, 2, 3].map { value => value * 2 }
println("${total} ${name} ${fallback.first} ${empty.len()} ${mapped.len()}")
}Forms
| Form | Selects |
|---|---|
function(args) |
A named function in scope |
value.method(args) |
A method for the receiver’s static type |
Type.function(args) |
A static function on the type |
callable(args) |
A function or closure value |
function[Types](args) |
A call with explicit generic arguments |
function(args) { closure } |
A call whose last argument is a trailing closure |
Parentheses are required even when there are no arguments — a bare name is a function value, not a call.
fn now(): int => 7
fn apply(op: fn() -> int): int => op()
fn main(): void {
called := now()
as_value := now
println("${called} ${apply(as_value)} ${apply(now)}")
}Using the name where a value is not expected is a type error, which is the diagnostic you get if you forget the parentheses:
fn now(): int => 7
fn main(): void {
v: int = now
println("${v}")
}Line breaks
A ( at the start of a fresh line does not continue the previous expression.
operation on one line and (value) on the next are two statements, and the
first one is just a function value:
fn twice(v: int): int => v * 2
fn main(): void {
value := 3
result: int = twice
(value)
println("${result}")
}Keep ( on the callee’s line. Inside an already open argument list, line
breaks are ordinary trivia, and a trailing comma is allowed:
fn combine(first: string, second: string, third: string): string =>
first + "-" + second + "-" + third
fn main(): void {
joined := combine(
"alpha",
"beta",
"gamma",
)
println(joined)
}A member chain may also break across lines, because the leading . makes the
connection explicit:
fn main(): void {
result := [3, 1, 2]
.sorted()
.map { v => v * 10 }
.len()
println("${result}")
}Evaluation
Call evaluation is deterministic:
- evaluate the callee or receiver, once;
- evaluate the written arguments left to right, once each;
- materialize omitted trailing defaults;
- enter the callable;
- produce its success value, error value, or suspension.
fn step(label: string, value: int): int {
println("evaluating ${label}")
return value
}
fn combine(a: int, b: int): int => a + b
fn main(): void {
total := combine(step("first", 20), step("second", 22))
println("total ${total}")
}That prints evaluating first, then evaluating second, then total 42. The
compiler does not reorder calls because one looks pure, and method lookup
never executes user code.
The whole argument list is established before the callee body begins, so a later argument cannot observe mutation performed inside the callee, and a call that fails while evaluating an argument never enters the callee at all.
Arity and defaults
Calls are positional: supply every required parameter and no extras.
fn add(a: int, b: int): int => a + b
fn main(): void {
println("${add(1)}")
}Defaults fill only an omitted trailing suffix, so a caller cannot skip port
and supply only secure:
fn connect(host: string, port: int = 5432, secure: bool = true): string {
scheme := if secure { "https" } else { "http" }
return "${scheme}://${host}:${port}"
}
fn main(): void {
println(connect("db.internal"))
println(connect("db.internal", 6432))
println(connect("db.internal", 6432, false))
}There are no named arguments and no variadic parameters — name: value inside
an argument list is not call syntax, and the diagnostic reflects that the
compiler is trying to read it as an ordinary expression:
fn connect(host: string, port: int): string => "${host}:${port}"
fn main(): void {
println(connect(port: 5432, host: "db.internal"))
}Default expressions are folded at compile time; they do not execute in the caller’s scope. Changing a public default changes the behaviour of source that still compiles unchanged, so treat it as API surface. When several optional settings evolve independently, take a struct — the field names then do the work named arguments would have:
struct ConnectOptions {
host: string
port: int
secure: bool
}
fn connect(options: ConnectOptions): string {
scheme := if options.secure { "https" } else { "http" }
return "${scheme}://${options.host}:${options.port}"
}
fn main(): void {
println(connect(ConnectOptions {
host: "db.internal",
port: 6432,
secure: true,
}))
}Arguments
Each argument is checked against its parameter’s expected type, and that expectation flows into the argument. Numeric literals adopt the parameter’s width, and a closure argument’s parameter types come from the function type.
fn write(port: u16, transform: fn(string) -> string): string {
return transform(" payload ") + "@${port}"
}
fn main(): void {
println(write(443, text => text.trim()))
}443 is created as a u16, and text is known to be a string without an
annotation. A mismatched argument is reported in argument position:
fn takes_int(value: int): int => value
fn main(): void {
println("${takes_int("nope")}")
}Mutable arguments
A mut parameter is an in-out place contract. The callee receives the current
value and its final value is written back.
fn append_marker(mut values: []string): void {
values.add("done")
}
fn bump(mut counter: int, by: int): void {
counter += by
}
fn main(): void {
mut names := ["start"]
mut total := 10
append_marker(names)
bump(total, 5)
println("${names.len()} ${names[1] ?? "?"} ${total}")
}The argument must be a mutable place — a mut binding, or a field or index of
one. A plain binding cannot satisfy it:
fn append_marker(mut values: []string): void {
values.add("done")
}
fn main(): void {
names := ["start"]
append_marker(names)
println("${names.len()}")
}Mutation through mut self uses the same model, and the receiver is still
evaluated once:
struct Queue {
items: []int
fn push(mut self, value: int): void {
self.items.add(value)
}
fn len(self): int => self.items.len()
}
fn main(): void {
mut q := Queue { items: [] }
q.push(1)
q.push(2)
println("${q.len()}")
}Generic arguments
Generic arguments are normally inferred from the argument types and the expected result:
fn identity[T](value: T): T => value
fn first_or[T](xs: []T, fallback: T): T => xs.get(0) ?? fallback
fn main(): void {
println("${identity(42)} ${identity("text")}")
println("${first_or([3, 4], 0)} ${first_or([], "none")}")
}Write them explicitly, immediately before (, when nothing constrains the
choice — most often when constructing an empty collection:
fn empty_of[T](): []T => []
fn main(): void {
names := List.new[string]()
counts := Map.new[string, int]()
seen := Set.new[int]()
blank := empty_of[float]()
println("${names.len()} ${counts.size()} ${seen.size()} ${blank.len()}")
}Square brackets not followed by ( remain indexing syntax, so the two forms
never collide. Each call receives an independent substitution: a call with
int does not specialize the declaration for a later call with string.
Method and static dispatch
Method selection uses the receiver’s static type together with inherent methods, receiver-prefix declarations, applicable trait implementations, and bounds in scope. It is resolved at compile time — there is no runtime method table.
struct Article {
title: string
body: string
}
impl Article {
fn summary(self): string => "${self.title} (${self.body.len()} chars)"
}
fn Article.headline(self): string => self.title
trait Sized {
fn approximate_size(self): int
}
impl Sized for Article {
fn approximate_size(self): int => self.title.len() + self.body.len()
}
fn report[T](item: T): string where T: Sized => "size ${item.approximate_size()}"
fn main(): void {
a := Article { title: "Atoll", body: "a systems language" }
println(a.summary())
println(a.headline())
println(report(a))
}Missing selection is a compile-time error naming the receiver type:
struct Point { x: float, y: float }
fn main(): void {
p := Point { x: 1.0, y: 2.0 }
println("${p.magnitude()}")
}Static functions have no receiver and are selected through the type:
struct Point {
x: float
y: float
fn zero(): Point => Point { x: 0.0, y: 0.0 }
fn along_x(distance: float): Point => Point { x: distance, y: 0.0 }
}
fn main(): void {
println("${Point.zero().x} ${Point.along_x(3.5).x}")
}Calling function values
A local, parameter, or field with function type is called with the same parentheses. The callable expression is evaluated once, so a call that produces a function can be invoked immediately:
struct Route {
path: string
handler: fn(int) -> int
}
fn choose_handler(kind: string): fn(int) -> int {
if kind == "double" {
return v => v * 2
}
return v => v + 1
}
fn main(): void {
r := Route { path: "/scale", handler: choose_handler("double") }
println("${r.handler(21)}")
println("${choose_handler("inc")(41)}")
}choose_handler("inc") runs first and yields a function value; the second
parentheses invoke it.
Trailing closures
When the last argument is a closure, it may be written in braces after the argument list instead of inside it. Both forms mean the same thing.
fn retry(times: int, body: fn(int) -> bool): bool {
for attempt in 0..times {
if body(attempt) {
return true
}
}
return false
}
fn main(): void {
inline_form := retry(3, attempt => attempt == 2)
trailing_form := retry(3) { attempt => attempt == 2 }
println("${inline_form} ${trailing_form}")
}The braces belong to the closure argument, not to the call’s result. This is what makes multi-line callbacks readable, and it is how the collection higher-order methods are normally written:
fn each_path(paths: []string, body: fn(string) -> void): void {
for p in paths {
body(p)
}
}
fn main(): void {
values := [1, -2, 3, -4]
positive := values.filter { v => v > 0 }
doubled := values.map { v => v * 2 }
total := values.reduce(0) { (acc, v) => acc + v }
println("${positive.len()} ${doubled.len()} ${total}")
each_path(["/health", "/metrics"]) { path =>
println("checking ${path}")
}
}Errors at a call site
Calling a fallible function produces a Result[T, E]. The caller must store,
match, convert, or propagate it.
error CfgError { Missing }
fn read(key: string): string ! CfgError {
if key == "" {
error Missing
}
return "value-of-${key}"
}
fn build(): string ! CfgError {
host := read("host")?
port := read("port")?
return "${host}:${port}"
}
fn main(): void {
println(build().unwrap_or("unconfigured"))
direct := read("") catch {
err => {
println("missing key")
return
}
}
println(direct)
}? applies after the call and exits the enclosing fallible function. Argument
evaluation that already happened is not rolled back when a call returns Err;
compensation stays an explicit policy. See Errors.
Effects at a call site
A call inherits the callee’s effect row, so ordinary call syntax may allocate, suspend, spawn, or exit through the error channel.
fn slow(value: int): int => value * 2
fn parallel(a: int, b: int): int {
left := spawn { slow(a) }
right := spawn { slow(b) }
return left.await() + right.await()
}
fn main(): void {
println("${parallel(2, 3)}")
}An enclosing declared using row must permit every inherited effect; without
one, the compiler infers the union transitively. If a call suspends, values
that are live afterwards are kept in the generated continuation — a
consequence of the callee’s signature, not of any special call punctuation.
A complete example
error FetchError { Offline, NotFound }
struct Client {
base: string
retries: int
fn get(self, path: string): string ! FetchError {
if path == "" {
error NotFound
}
if self.retries == 0 {
error Offline
}
return "${self.base}${path}"
}
}
impl Client {
fn local(): Client => Client { base: "http://localhost", retries: 3 }
}
fn with_retries(c: Client, n: int): Client => Client { base: c.base, retries: n }
fn each_path(paths: []string, body: fn(string) -> void): void {
for p in paths {
body(p)
}
}
fn main(): void {
client := with_retries(Client.local(), 2)
each_path(["/health", "/metrics", ""]) { path =>
body := client.get(path) catch {
err => {
println("${path} -> failed")
return
}
}
println("${path} -> ${body}")
}
}Every call form appears here: a static function through the type
(Client.local()), a free function (with_retries), a method on a value
(client.get), a trailing-closure call (each_path(...) { ... }), a call to
a function value (body(p)), and a fallible call discharged with catch.
Diagnostics
Call diagnostics name the contract that failed, and each one has its own fix:
| Diagnostic | Cause | Fix |
|---|---|---|
| unresolved name / no method on type | The callee or method does not exist for that type | Import it, or add the method |
| takes N arguments, but M supplied | Wrong arity | Match the signature; defaults only fill a trailing suffix |
| type mismatch in argument position | Incompatible argument | Convert explicitly, or change the parameter |
| must be a mutable place | Immutable binding passed to a mut parameter |
Declare the binding mut |
| unsatisfied bound | The type argument does not implement a required trait | Add the implementation or @derive it |
unhandled Result |
A fallible call whose error channel was ignored | ?, catch, or unwrap_or |
Adding a cast does not repair missing mutability, a missing error boundary, or an unsatisfied bound — fix the dimension the diagnostic names.