TYCHO Search

Search documentation

    ↑ ↓ to move · Enter to open · Esc to close

    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:

    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.

    shapewhat crosses the callbacking storage
    int, float, bool, charthe value—
    string, bytes, [T], mapdescriptorshared, read-only
    struct/enum/newtype with heap fieldsvalue with descriptorsshared, read-only
    [N]T, vector[N]T, struct with an inline arraythe whole valuecopied
    inout Tthe caller's placemutated in place
    sink Townershipmoved, no 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 typeconsume rule
    string, [T], a struct with any heap field, a newtype over oneenforced
    int, float, bool, a struct of scalars only, a newtype over a scalarnot 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.