Skip to content

Functions

fn fib(n: i64) -> i64 {
if n < 2 {
return n
}
return fib(n - 1) + fib(n - 2)
}
fn greet(name: str) { // no `-> R`: returns unit
print("hello, " + name)
}

fn name(param: Type, ...) -> Result { body } declares a function. Every parameter has a written type, and so does the result; without -> Result the function returns unit. A function that returns a value must return on every path.

Functions are module-level items, declared in any order: a function can call one declared further down, and functions can be mutually recursive. They are private to their module unless declared pub fn.

Between the signature and the body, requires and ensures clauses can state a contract, which the compiler tries to break while compiling and the program checks as it runs; nocopy says that natively the function copies no list, string or closure; and replaces makes it a rewrite rule that calls matching a pattern use instead, once the compiler has checked it computes the same.

There is no overloading, no default arguments, and no methods: functions that work on a type take it as a parameter, usually first (push(v, x); for a struct to be changed, push(&v, x) in compiled code).

f(a, b) evaluates the arguments from left to right, then calls f. Arguments are passed by value: a function gets its own copy of every struct and array, so it cannot change the caller’s, and changing a parameter (n = n - 1) changes only the function’s copy. To let a function change something, pass a pointer in compiled code or a cell in hosted code.

The VM allows 10 000 nested calls; deeper recursion stops the program with stack overflow. Compiled code is limited by the size of the OS stack.

A function that returns a call, return g(...) with nothing done to the result, does not nest: g takes the place of the caller, which is gone before g runs, so a chain of such calls runs in constant stack however deep it goes. A call of the function itself becomes a jump back to its start with the new arguments; a call of another function, as in mutual recursion or a state machine, is a tail call, whatever the two functions’ parameters.

fn count(n: i64, acc: i64) -> i64 {
if n == 0 {
return acc
}
return count(n - 1, acc + 1) // a jump: count(10000000, 0) is fine
}
fn depth(n: i64) -> i64 {
if n == 0 {
return 0
}
return 1 + depth(n - 1) // not a tail call: the result is used
}
fn is_even(n: i64) -> bool {
if n == 0 { return true }
return is_odd(n - 1) // a tail call: is_even(10000000) is fine
}
fn is_odd(n: i64) -> bool {
if n == 0 { return false }
return is_even(n - 1)
}

This is a rule of the language, the same on the VM, natively and freestanding, at every -O level, not an optimization that may or may not happen. It holds anywhere in the function: in either branch of an if, in a match arm, inside a loop. Lists and strings passed along are moved into the parameters or handed over to the callee, and natively whatever else the round made is freed before the jump or tail call, so a tail call that would have had to free something after returning is one all the same. Calls of C functions and of function values (return f(x) with f a parameter) are ordinary calls, and so is every return g(...) in a function with an ensures clause, which has to check what g returned (Contracts).

A program starts at main (hosted, native) or _start (freestanding), which returns nothing or an i64 exit status. See Profiles.

A parameter marked comptime is given at compile time, and makes the function generic: it is compiled once for each distinct set of compile-time arguments.

fn max(comptime T: type, a: T, b: T) -> T {
if a > b { return a }
return b
}
let m = max(u8, 3, 200)

See Generics. A comptime parameter of a function type makes calls to it direct, and lets a closure be passed without the GC.

A function’s name, used without calling it, is a value of type fn(params) -> result that can be stored, passed and returned:

fn apply(f: fn(i64) -> i64, x: i64) -> i64 {
return f(x)
}
apply(fib, 10)

See Function values and Closures, which also covers anonymous functions (fn(x: i64) -> i64 { return x * x }).

Some names, such as print, len and syscall, look like functions but are built into the compiler: they may accept several types and cannot be used as values. They are listed in Builtins.