Skip to content

Functions

kairo-docs-bot edited this page Jul 12, 2026 · 6 revisions

Functions

Functions in Kairo follow a consistent declaration syntax for both free functions and class methods. The full grammar covers visibility, ABI linkage, generics, modifiers, bounds, and return types all optional except the fn keyword, the name, and the parameter list.


Declaration Syntax

fn name(param1: Type1, param2: Type2) -> ReturnType {
    // body
}

The full grammar:

function_declaration ::= visibility? abi_mod? "fn" generics? identifier
                         "(" parameter_list? ")" function_mods? ("->" return_type)? constraint? body

visibility    ::= "pub" | "priv" | "prot"
abi_mod       ::= ("ffi" string_literal) | "static" | "virtual" | "override"
generics      ::= "<" generic_param_list ">"
function_mods ::= ("const" | "volatile" | "unsafe" | "eval" | "async" | "final" | "panic" | "inline")*
constraint    ::= "requires" expression
return_type   ::= type | "!"
body          ::= block | "=" expression | ε        ; ε = forward declaration

All parts except fn, the name, and the parenthesized parameter list are optional. When the return type is omitted, it defaults to void.


Parameters

Parameters are declared as name: Type. Each parameter requires an explicit type annotation there is no parameter type inference.

fn greet(name: string, loud: bool) {
    if loud {
        std::println(f"HELLO, {name}!")
    } else {
        std::println(f"Hello, {name}.")
    }
}

Default parameters

Parameters can have default values. When a caller omits a defaulted argument, the default is used:

fn greet(name: string = "world") {
    std::println(f"Hello, {name}!")
}

greet()          // "Hello, world!"
greet("Alice")   // "Hello, Alice!"

Defaults are evaluated at the call site. Parameters with defaults must appear after non-defaulted parameters.

Named arguments

Arguments can be passed by name at the call site. Positional arguments must come before named arguments, matching C++ conventions:

fn create_user(name: string, age: i32 = 18, country: string = "USA") -> User {
    return User { name, age, country }
}

create_user("Alice")                              // age=18, country="USA"
create_user("Bob", 25)                            // country="USA"
create_user(name: "Eve", country: "UK")           // age=18
create_user("Grace", 22)                          // country="USA"
create_user(name: "Frank", age: 30, country: "CA") // all explicit

Return Types

Explicit return

fn add(a: i32, b: i32) -> i32 {
    return a + b
}

Implicit void

When no return type is specified, the function returns void:

fn log(msg: string) {
    std::println(msg)
}

fn log_explicit(msg: string) -> void {   // equivalent
    std::println(msg)
}

No-return !

Functions that never return they always panic, loop forever, or call a no-return function use ! as their return type:

fn fatal(msg: string) -> ! {
    std::println(f"Fatal: {msg}")
    std::crash(1)
}

fn event_loop() -> ! {
    loop {
        process_events()
    }
}

! is the no-return type. It is a subtype of every type, meaning it can appear anywhere a value is expected:

var x: i32 = if valid { compute() } else { fatal("bad state") }
// fatal() returns ! which coerces to i32

Warning

No-return functions cannot have a panic specifier. Since panic acts as an alternative return path, it contradicts the guarantee that the function never returns. The compiler rejects fn f() panic -> !.

Special return types

These return type modifiers interact with Kairo's concurrency and type system. Each is covered in detail on its respective page:

Return type Description Details
yield T Coroutine yields values of type T cooperatively Concurrency
atomic T Atomic wrapper thread-safe operations Concurrency
thread T Thread-local storage Concurrency
fn generate_numbers() -> yield i32 {
    for i in 0..10 {
        yield i   // yield, not return function must have a yield return type
    }
}

Note

Functions with a yield return type cannot use return to produce values only yield. A bare return (no operand) is permitted to terminate the coroutine early.


Expression-Bodied Functions

Single-expression functions can use the = shorthand, omitting braces and return:

fn add(a: i32, b: i32) -> i32 = a + b
fn square(x: f64) -> f64 = x * x
fn greeting(name: string) -> string = f"Hello, {name}!"

The return type annotation is optional for expression-bodied functions it is inferred from the expression when omitted. This is the only form of return type inference in Kairo; block-bodied functions always require an explicit return type (or default to void).

fn add(a: i32, b: i32) = a + b              // inferred as i32
fn greeting(name: string) = f"Hi, {name}!"  // inferred as string

Explicit annotations are still useful when you want to constrain or widen the inferred type:

fn promote(x: i32) -> i64 = x as i64        // would otherwise infer i32

Function Return

return exits the current function with a value. For void functions, return takes no operand.

fn max(a: i32, b: i32) -> i32 {
    if a > b {
        return a
    }
    return b
}

fn log(msg: string) {
    std::println(msg)
    return   // explicit return from void function valid but optional
}

Early returns are permitted anywhere in a function body. The compiler verifies that all code paths return a value of the declared return type.


Function Overloading

Functions can be overloaded by parameter types, matching C++ overload resolution rules:

fn add(a: i32, b: i32) -> i32 = a + b
fn add(a: f64, b: f64) -> f64 = a + b
fn add(a: string, b: string) -> string = a + b

Unsafe overloads

The unsafe modifier creates a separate overload in its own namespace. Safe and unsafe versions of the same function coexist the caller explicitly selects which one to invoke:

fn add(a: i32, b: i32) -> i32 {
    return a + b
}

fn add(a: i32, b: i32) unsafe -> i32 {
    return a << 1 + b << 1   // faster but semantically different
}

var x = add(10, 20)          // calls the safe version
var y = unsafe add(10, 20)   // calls the unsafe overload

Note

unsafe overloads are not "unsafe memory" AMT still guarantees memory safety. The unsafe qualifier signals that the function may not uphold other invariants that the safe version does. See Unsafe for the full unsafe model.

Const overloading restriction

const and non-const methods with the same name and parameter types cannot coexist, use distinct names like get() and get_mut(). This restriction applies to named methods only. Operators are exempt, because they cannot be renamed: a place-returning operator may declare both a self overload (returning *T) and a const self overload (returning *const T), dispatched by receiver const-ness. See Operators.

class Foo {
    fn bar(const self) -> i32 { return 42 }
    fn bar(self) -> i32 { return 24 }               // compile error: cannot overload const
    fn bar(const self, a: i32) -> i32 { return a }  // ok: different parameter list ok

    fn op [](const self, index: i32) -> i32 { return 42 }   // ok: operator overload
    fn op [](self, index: i32) -> i32 { return 24 }         // ok: const-ness dispatch
}

Variadic Functions

The ... prefix on a parameter name accepts an arbitrary number of arguments of the same type. The parameter is accessible as a tuple inside the function body:

fn sum(...numbers: i32) -> i32 {
    var total = 0
    for num in numbers {
        total += num
    }
    return total
}

sum(1, 2, 3)        // 6
sum(10, 20, 30, 40) // 100

Generic variadic functions

Combine ... with generic type packs to accept arguments of different types:

fn <...T> print_all(...args: T) {
    for arg in args {
        std::println(arg as string)
    }
}

print_all(42, "hello", true)   // prints each on a new line

The parameter is a tuple of heterogeneous types. Each element in the pack must satisfy the constraints used in the function body in the example above, every T must be convertible to string via as.

Pack expansion and forwarding

A pack expands with a postfix ... in expression position — declaration is prefix (...args), use is postfix (args...). The most common case is forwarding a pack to another call:

fn <...T> log_all(...args: T) {
    print_all(args...)         // forwards every element as a separate argument
}

The same postfix form appears in type position for pack-typed signatures. The split mirrors the token-macro splat: ... before a name declares a pack, ... after an expression expands one.


Generic Functions

Generic functions declare type parameters in angle brackets before the function name:

fn <T> identity(x: T) -> T {
    return x
}

Constrain type parameters with impl (interface conformance) or derives (class inheritance):

fn <T impl Comparable> max(a: T, b: T) -> T {
    return if a > b { a } else { b }
}

fn <T derives Serializable> serialize(value: T) -> [byte] {
    return value.to_bytes()
}

Note

impl checks structural conformance the type satisfies the interface's required method signatures without needing an explicit impl declaration. derives checks polymorphic inheritance the type is a subclass of the specified class. See Interfaces and Requires Clauses for details.

Requires Clauses

For constraints beyond type parameter bounds, attach a requires clause after the return type:

fn <T impl ToString> print_value(value: T) requires sizeof T <= 64 {
    std::println(value as string)
}

requires is a compile-time gate: the condition must hold at compile time or the program does not compile — no runtime fallback, no dispatch. See Requires Clauses for the full constraint system and Where Clauses for runtime-conditional overload dispatch.


Function Modifiers

Modifiers appear after the parameter list and before the return type arrow. Multiple modifiers can be combined, subject to the compatibility rules below.

fn compute(x: i32) const inline -> i32 { return x * x }
fn dangerous() unsafe -> void          { /* ... */ }
fn compile_time() eval -> i32          { return 42 }
fn may_fail() panic -> i32             { /* ... */ }
fn background() async -> Data          { /* ... */ }

Modifier reference

Modifier Free functions Methods Description
const Yes Method does not modify self. See Variables
volatile Yes Yes Prevents certain compiler optimizations; for hardware interaction
unsafe Yes Yes Separate overload namespace for alternative implementations
eval Yes Yes Must be evaluable at compile time. See Eval
async Yes Yes Asynchronous execution. See Concurrency
panic Yes Yes May panic; callers must handle. See Panic
inline Yes Yes Hint to inline at call sites
final Yes Prevents override in subclasses. See Classes

Modifier compatibility

Not all modifiers can be combined:

Combination Valid Reason
const + volatile ok:
const + unsafe unsafe: implies a separate overload with different invariants
const + eval eval: implies compile-time evaluation const self is meaningless
const + async ok:
unsafe + any other ok: unsafe: creates a separate overload the other modifiers apply to both versions as appropriate
eval + unsafe ok: eval and unsafe are orthogonal modifiers
eval + any other (except unsafe) eval implies compile-time evaluation incompatible with runtime modifiers
async + const ok:
async + volatile ok:

Visibility

Keyword Scope
pub Accessible from any module
priv Accessible only within the defining module (default)
prot Accessible within the defining module and by subclasses in other modules
pub fn public_api()       { /* ... */ }
priv fn internal_helper() { /* ... */ }
prot fn for_subclasses()  { /* ... */ }

Visibility applies to both free functions and methods. See Modules for how visibility interacts with imports.


ABI and Linkage

ABI modifiers control name mangling, dispatch mechanism, and symbol visibility at the object code level.

Modifier Description
ffi "c" C linkage no name mangling. See C/C++ Interop
ffi "c++" C++ linkage Itanium or MSVC mangling. See C/C++ Interop
static Internal linkage; no vtable dispatch. Cannot be virtual or override
virtual Dynamic dispatch via vtable. See Classes
override Overrides a virtual method from a base class; implies virtual

static, virtual, and override are mutually exclusive with each other. ffi can be combined with any of them.

class Shape {
    virtual fn area(self) const -> f64 { return 0.0 }
}

class Circle : Shape {
    var radius: f64

    override fn area(self) const -> f64 {
        return 3.14159 * self.radius * self.radius
    }
}

class Math {
    static fn sqrt(x: f64) -> f64 { /* ... */ }
}

Math::sqrt(16.0)   // called without an instance

Function Pointers

Functions are first-class values. The type of a function pointer is fn(ParamTypes) -> ReturnType:

fn add(a: i32, b: i32) -> i32 = a + b
fn sub(a: i32, b: i32) -> i32 = a - b

var op: fn(i32, i32) -> i32 = add
op(3, 4)   // 7

op = sub
op(10, 3)  // 7

Functions can be nested inner functions are scoped to the enclosing function:

fn outer(x: i32) -> i32 {
    fn inner(y: i32) -> i32 = y * 2
    return inner(x) + 1
}

Closures

Anonymous functions (lambdas) capture variables from the enclosing scope. Default capture is by copy; use |&| for capture-by-reference or specify per-variable:

var multiplier = 3
var scale = fn (x: i32) -> i32 { return x * multiplier }   // captures multiplier by copy

scale(10)   // 30

See Closures for capture modes (|&|, |a, &b|), lifetime rules, and how closures interact with AMT.


Operator Functions

Operators are overloaded with the fn op syntax:

class Vec2 {
    var x: f64
    var y: f64

    fn op +(self, other: Vec2) -> Vec2 {
        return Vec2 { x: self.x + other.x, y: self.y + other.y }
    }
}

See Operators for the full list of overloadable operators, special operator syntax (l++/r++, op as, op in, op delete), and restrictions.


Forward Declarations

A function can be declared without a body a signature followed by no block:

fn parse_expression(tokens: [Token]) -> Expr
fn parse_statement(tokens: [Token]) -> Stmt

Within a single module, forward declarations are rarely needed. Kairo hoists all top-level declarations before type checking, so mutual recursion works without them:

fn is_even(n: u32) -> bool = if n == 0 { true } else { is_odd(n - 1) }
fn is_odd(n: u32) -> bool  = if n == 0 { false } else { is_even(n - 1) }

Forward declarations are used when the definition lives elsewhere:

  • FFI imports the body is provided by a C or C++ library. See C/C++ Interop.

  • Separate translation units the declaration is visible to callers; the definition is linked in from another .kro file.

  • Out-of-line class methods declared in the class body, defined outside it using the Class::method qualified-name syntax:

    class Parser {
        var pos: usize
        fn advance(self)    -> Token
        fn peek(self) const -> Token
    }
    
    fn Parser::advance(self) -> Token {
        var t = self.tokens[self.pos]
        self.pos += 1
        return t
    }
    
    fn Parser::peek(self) const -> Token = self.tokens[self.pos]
    

    See Classes for the full rules.

Signature matching

When a definition follows a forward declaration, the two must match exactly:

  • Parameter types, return type, and all function modifiers (const, unsafe, panic, eval, async, inline, final, volatile)
  • Visibility (pub, priv, prot)
  • ABI linkage (ffi "c", ffi "c++", static, virtual, override)

Parameter names and default values may differ the declaration's names and defaults are used at call sites that see only the declaration; the definition's are used everywhere else. For consistency, keep them the same.

A mismatch in any other element is a compile error.


Summary

// Basic function
fn add(a: i32, b: i32) -> i32 = a + b

// Default parameters + named arguments
fn connect(host: string = "localhost", port: i32 = 8080) { /* ... */ }
connect(port: 9090)

// Generic with bounds
fn <T impl Printable> show(value: T) { std::println(value as string) }

// Variadic
fn <...T> log(...args: T) { /* ... */ }

// Overloaded
fn process(x: i32) -> i32 { /* ... */ }
fn process(x: string) -> string { /* ... */ }

// Unsafe overload
fn process(x: i32) unsafe -> i32 { /* ... */ }

// Method with modifiers
class Server {
    pub fn start(self) async panic { /* ... */ }
    pub fn status(self) const -> string { /* ... */ }
    pub static fn default_port() -> i32 = 8080
}

// Function pointer
var handler: fn(Request) -> Response = handle_request

// No-return
fn abort() -> ! { std::crash(1) }

Start here: Primitives


1. Fundamentals

2. Functions & Control Flow

3. Types

4. Modules & Metaprogramming

5. Memory & Safety

6. Interop & Concurrency


Website · Docs · Repo

Clone this wiki locally