Skip to content

Functions

Functions are declared with def. Parameters are written type name (type before name), and the return type goes between the ) and the { (Go-style trailing return type):

def add(int a, int b) int {
return a + b
}
def greet(string name) { # no return type → returns no value
println("hi " + name)
return # value-less return for early exit
}
  • A function call is an expression of the return type’s value, usable in expressions, assignments, and conditions.
  • Using the result of a no-value function is a compile error.
  • In a value-returning function, the type of return expr must match the declared return type exactly.
  • Every control-flow path of a value-returning function must end in return expr. If any path could reach the end of the body without returning, it is a compile error (the same strictness as Go and Rust).
    • A block definitely returns if its last statement definitely returns. An if/else definitely returns only if both branches do. An if without else, and a while, are not considered to definitely return.
  • A return expr in a no-value function, and a value-less return in a value-returning function, are both errors.

Top-level functions are compiled in two passes: pass 1 collects every def signature, pass 2 compiles the bodies. As a result, forward references, mutual recursion, and self-recursion all work — a function may call another defined later in the file. Duplicate definitions are caught in pass 1. (Global variables still must be declared before use; only function names are order-independent.)

Calls are checked statically (exit 65) for undefined functions, argument count, and argument types. There are no implicit conversions at the call site.

A parameter may have a default value, which must be a literal and must come after all required parameters:

def connect(string host, int port = 8080) {
# ...
}
connect("localhost") # port defaults to 8080
connect("localhost", 3000) # port is 3000
  • The default value must be an int, float, bool, string, or null literal — a function call, arithmetic expression, or identifier is a compile error ("default value for 'x' must be a literal").
  • A required parameter cannot follow a defaulted one ("required parameter 'y' cannot follow a default parameter").
  • Default arguments apply to ordinary functions called by name. Lambdas, function values, methods, and cross-module calls use strict arity.

Arguments may be passed by name:

def make(int x, int y) Point { return Point { x: x, y: y } }
make(x: 1, y: 2)
make(1, y: 2) # positional then named is OK
  • Positional arguments may be followed by named ones; a positional argument after a named one is a parse error.
  • Duplicate or unknown argument names are compile errors.
  • Function-typed values (lambdas) have no parameter names, so named arguments do not apply to them.

Functions are first-class values: you can store them in variables, pass them as arguments, return them, place them in struct fields, and put them in arrays.

auto double_fn = (int x) => x * 2 # single-expression body
auto absv = (int x) => { # block body
if (x < 0) { return x * -1 }
return x
}
auto greet = () => "hello" # no parameters

A function type is written (T1, T2) => R:

(int) => int f = (int x) => x + 1
(int, int) => bool less = (int a, int b) => a < b

Function types are structurally equal — they match on shape (parameter list plus return type), not by name.

Functions can take and return other functions, hold them in struct fields, and collect them in arrays:

def apply(int n, (int) => int fn) int { return fn(n) }
def make_adder(int n) (int) => int {
return (int x) => x + n # a closure capturing n
}
struct Transform { string name (int) => int fn }
array<(int) => int> pipeline = [
(int x) => x * 2,
(int x) => x + 10,
(int x) => x * x
]
[1, 2, 3, 4, 5].map((int x) => x * 2) # [2, 4, 6, 8, 10]
[1, 2, 3, 4, 5].filter((int x) => x > 2) # [3, 4, 5]
[1, 2, 3, 4, 5].reduce(0, (int acc, int x) => acc + x) # 15

Lambdas capture variables from the enclosing scope. Captured variables are shared: if two closures capture the same local, a write through one is visible through the other — this makes the make_counter pattern work. Capture is transitive across nested lambdas. A struct method’s lambda may not capture self.

In a higher-order call, the lambda’s parameter types may be omitted and inferred from the expected function type:

apply(5, (x) => x + 1) # x inferred as int from apply's (int) => int
[1, 2, 3].map((x) => x * 2) # x inferred from map's element type

A mismatch with an explicit type, or a lambda with no annotation and no inferable context, is a compile error.

Any function value can be called with (...), including a bare def name used as a value:

def square(int x) int { return x * x }
[1, 2, 3].map(square) # square referenced as a value
t.fn(5) # a function stored in a struct field
arr[0](3) # a function stored in an array

Always-succeeding conversions return a bare value:

  • to_string(int | float | bool) -> string
  • to_int(float) -> int — truncates toward zero
  • to_float(int) -> float

Fallible conversions return a Result<T>:

  • to_int(string) -> Result<int>
  • to_float(string) -> Result<float>
  • to_bool(string) -> Result<bool>
Result<int> r = to_int("42")
if (r is ok) { println(r.value) } # 42
  • string → int / float parses the whole string and rejects partial matches, empty strings, and surrounding whitespace ("42 " and "42x" both error). to_int(string) also rejects integer overflow.
  • to_bool(string) accepts only "true" / "false", case-sensitively.
  • to_int(bool), to_float(bool), to_bool(int), and to_bool(float) are compile errors — there is no conversion path between bool and numbers.

to_string accepting several argument types is an internal mechanism for built-ins only; user-defined functions cannot be overloaded (one name = one definition).

  • println(x) — print x followed by a newline.
  • print(x) — print x with no trailing newline.

Both accept any value type with a single argument and return no value. Formatting matches the value printer: int in decimal, float with %g plus .0, bool as true/false, string without quotes, array<T> as [e1, e2, e3], Result<T> as ok(x) / err(msg), and a nullable as the value or null.

The string type also supports f-strings for interpolation:

string name = "world"
println(f"Hello {name}!") # Hello world!

For positional/indexed formatting, see std/strings’s format in the Standard library overview.