15. Functions
The grammar of function declarations, parameters, and the where clause is in §4.1.1. This chapter defines parameter passing, variadics, first-class function values, method-call syntax, and subscripts.
15.1 Declarations
A function has a name, a parameter list, an optional -> R return type (absent means void), an optional where clause (for a generic function, §7.2), and a block body. A function that mentions a $T or $N in its signature is generic (§7). A function MUST NOT return a handle (§25), and MUST NOT declare more than 16 parameters (the same cap for an extern fn; a *function type*, which is a distinct construct, allows up to 8 — §5.3.8). The 16-parameter cap applies to a generic template exactly as it does to a concrete function: being generic defers what the parameters *are*, never how many there may be. Exactly one function named main, with no parameters and void return, is the program entry point (§27).
15.2 Parameter passing modes
A parameter is passed in one of three modes:
- Default (read-only). A scalar is copied. A heap-bearing value — a
string,bytes, a dynamic array[T], a map, or a struct containing one — is passed as a read-only descriptor: the descriptor is copied, the backing storage is shared, and mutating the parameter is a compile error. A fixed array[N]T, avector[N]T, or a struct that inlines one has no heap descriptor to share: the whole value is copied. The caller's value is unchanged either way. A value produced or returned by the callee is copied into the appropriate storage as usual.
The call-site cost is the size of the emitted value: a descriptor is a fixed few words, while [N]T and inline-array structs copy all N elements. Use inout for a mutable borrow and sink to move ownership when that copy is the cost you are trying to avoid. The table below is the boundary; "large value" is not a separate rule.
| shape | what crosses the call | backing storage |
|---|---|---|
int, float, bool, char | the value | — |
string, bytes, [T], map | descriptor | shared, read-only |
| struct/enum/newtype with heap fields | value with descriptors | shared, read-only |
[N]T, vector[N]T, struct with an inline array | the whole value | copied |
inout T | the caller's place | mutated in place |
sink T | ownership | moved, no copy |
inout(copy-in/copy-out borrow). Specified in §11: an exclusive, call-scoped mutable borrow written&xat the call site, equivalent tox = f(x), with the exclusivity rule of §11.2. Aninoutparameter is forwarded to anotherinoutparameter with&again at the nested call site (bump(&n)); the exclusivity rule applies at each call.sink(owned, consuming). The callee owns and consumes the argument and may mutate it in place. On a heap-bearing type the caller's argument is consumed: after passing a variable to asinkparameter, the caller MUST NOT use that variable again (doing so is a compile error, not a silent copy).
The consume rule applies exactly to the heap-bearing types. A value with nothing on the heap has nothing to move; sink on it is accepted but not enforced.
Rationale: moving a scalar would copy no heap storage, so there is nothing to protect.
| parameter type | consume rule |
|---|---|
string, [T], a struct with any heap field, a newtype over one | enforced |
int, float, bool, a struct of scalars only, a newtype over a scalar | not enforced — the variable stays usable |
sink on a scalar is legal and means nothing. The meaning of a signature can change when a type gains a field: fn take(p: sink Point) over Point{int, int} enforces nothing, and starts enforcing consumption on every caller if Point gains a string field. A fresh literal or a local on its last use is adopted with no copy; a copy is made only where value semantics require independence (the variable is read again after the call, or is captured by a closure). Inside a loop a named variable is neither adopted nor silently copied — the call is REFUSED, because a copy hidden in a loop is the one place the move-vs-copy cost is invisible and unbounded; pass a fresh value (a literal, or a call's result) or lift the call out. A value that then escapes (is returned or stored past the call) is still copied to the longer-lived storage.
sink and inout are mutually exclusive on one parameter, and neither may combine with a variadic parameter.
15.3 Variadic parameters
A final parameter written ...T has type [T]; a call packs its trailing positional arguments into that array. f() supplies an empty []T; an existing [T] is forwarded with the spread form f(xs...). Fixed parameters may precede the variadic one, which MUST be last. The generic form ...$T infers T from the arguments; an EMPTY call to one has nothing to infer from, so it is an error unless the type is named explicitly — f$(int)() supplies the empty []int and is the cure the diagnostic points at (§7.5). A variadic parameter cannot be inout or sink. Variadics are pure sugar: the call folds the trailing arguments into one array argument, after which it is an ordinary call.
The packing is a property of the CALL, not of how the callee was named: a package-qualified call packs identically (vp.sum(1, 2, 3)), and so does one carrying explicit type arguments.
15.4 First-class function values
A function name and a lambda both denote a function value of a function type (§5.3.8), which may be bound, passed, stored, and called. A lambda's body is a single expression. Closures capture their free variables by deep copy at creation, and an escaping closure's environment is re-homed into the caller's storage on return (§13.6). Two things cannot be taken as function values: a builtin name (len, push, str, …), and a function with an inout parameter (an inout borrow is call-scoped and cannot be deferred through an indirect call). Function values are not comparable (§5.5).
15.5 Methods (UFCS)
Any free function may be called in method position: x.f(a, b) means f(x, a, b) — unless the receiver's struct has a function-typed field named f, in which case that stored function value is called instead (the field wins). Method-call syntax is uniform function-call syntax — Tycho has no separately-declared methods. For a generic function, the receiver is matched against the template's first-parameter pattern and the function is then instantiated (§7.6). Method-call chaining (x.f().g()) follows from the left-associative postfix grammar.
15.6 Subscripts
A subscript (§4.1.3, §18) declares a user-defined projection — a compile-time place-macro that yields a place rooted in one of its parameters, generalizing the built-in &m[k]. It is called in method position, usable as a place or an rvalue, and introduces no run-time object and no new aliasing. Its rules (the yielded place rooted in a parameter, each parameter used at most once) are specified in §18.