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
| 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. panic(E1, E2) declares the set. See Panic |
inline | Yes | Yes | Puts the body in the interface. See inline |
final | Yes | Prevents 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
inlinefunction must have a body, in the same file as its declaration (an in-class declaration may be defined out of line in that file). Aninlinedeclaration with no body is an error. - Every declaration of the function must agree on
inline. - Unused
inlinebodies cost parse time only: no code is generated for one nothing calls.
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
A top-level function with no visibility written is pub.
| Keyword | Scope of a top-level function |
|---|---|
pub (default) | Any module that imports this module |
priv | Current file only |
prot | Current 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.
| 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(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
.kfile. -
Out-of-line class methods declared in the class body, defined outside it using the
Class::methodqualified-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.