Function Declarations

Function Declarations

This page covers function modifiers, visibility, ABI and linkage, and forward declarations.

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. inline and eval may also be written before fn (pub inline fn twice(x: i32) -> i32); both positions mean the same thing.

fn compute(x: i32) 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

ModifierFree functionsMethodsDescription
constYesMethod does not modify self. See Variables
volatileYesYesPrevents certain compiler optimizations; for hardware interaction
unsafeYesYesSeparate overload namespace for alternative implementations
evalYesYesMust be evaluable at compile time. See Eval
asyncYesYesAsynchronous execution. See Concurrency
panicYesYesMay panic; callers must handle. panic(E1, E2) declares the set. See Panic
inlineYesYesPuts the body in the interface. See inline
finalYesPrevents override in subclasses. See Classes

inline

inline puts the body in the interface: importers compile it themselves. Use it for small functions on hot paths, and for functions the C++ compiler calls on your behalf (coroutine promises and awaiters).

pub inline fn twice(x: i32) -> i32 { return x * 2 }

Every module that can see the declaration gets the definition and compiles it, so the optimizer can inline the call there. In C++ terms it is an inline function: one shared definition that the linker merges across objects. It is not a request to force inlining.

  • An inline function must have a body, in the same file as its declaration (an in-class declaration may be defined out of line in that file). An inline declaration with no body is an error.
  • Every declaration of the function must agree on inline.
  • Unused inline bodies cost parse time only: no code is generated for one nothing calls.

Modifier compatibility

Not all modifiers can be combined:

CombinationValidReason
const + volatileok:
const + unsafeunsafe: implies a separate overload with different invariants
const + evaleval: implies compile-time evaluation const self is meaningless
const + asyncok:
unsafe + any otherok:unsafe: creates a separate overload the other modifiers apply to both versions as appropriate
eval + unsafeok:eval and unsafe are orthogonal modifiers
eval + any other (except unsafe)eval implies compile-time evaluation incompatible with runtime modifiers
async + constok:
async + volatileok:

Visibility

A top-level function with no visibility written is pub.

KeywordScope of a top-level function
pub (default)Any module that imports this module
privCurrent file only
protCurrent file and sibling files in the same library subtree
fn api()                  { /* ... */ }   // pub by default
priv fn internal_helper() { /* ... */ }
prot fn library_helper()  { /* ... */ }

See Modules for how visibility interacts with imports. Methods follow the member rules in Classes.


ABI and Linkage

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

ModifierDescription
ffi "c"C linkage no name mangling. See C/C++ Interop
ffi "c++"C++ linkage Itanium or MSVC mangling. See C/C++ Interop
staticInternal linkage; no vtable dispatch. Cannot be virtual or override
virtualDynamic dispatch via vtable. See Classes
overrideOverrides 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(const self) -> f64 { return 0.0 }
}

class Circle derives Shape {
    var radius: f64

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

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

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

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 .k 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(const self) -> Token
    }
    
    fn Parser::advance(self) -> Token {
        var t = self.tokens[self.pos]
        self.pos += 1
        return t
    }
    
    fn Parser::peek(const self) -> 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 may differ; the definition's names are used in the body. So may the names of generic parameters: fn <T impl Graph> g(x: T) -> i32 and a later fn <U impl Graph> g(x: U) -> i32 { ... } are one function. Their bounds may not: a declaration and its definition must name the same bounds for each generic parameter. (Two generic functions that both have bodies and differ only in their bounds are two overloads.) Defaults are not part of the match: each parameter's default may be written on any one declaration, and it applies to every call (see Defaults apply to the whole function).

A mismatch in any other element is a compile error.

See also