Skip to content
CryoCryo home
LanguageFoundations

4Functions

4.1 Function Declarations

function add(a: int, b: int) -> int {
    return a + b;
}

function greet(name: string) -> void {
    printf("Hello, %s!\n", name);
}

function main() -> int {
    greet("Cryo");
    return 0;
}

The return type follows the parameter list, separated by ->. If you omit it, the function returns void. Parameters take the form name: Type and may not be inferred.

Functions can be recursive, and there is no forward-declaration requirement: the compiler collects every function signature in a dedicated pass before type-checking any body, so call ordering in source is irrelevant.

4.2 Generic Functions

Type parameters appear in angle brackets after the name. They are available throughout the signature and body.

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

function swap<T>(a: T*, b: T*) -> void {
    const temp: T = *a;
    *a = *b;
    *b = temp;
}

At the call site, supply the concrete type:

const n: int    = identity<int>(42);
const s: string = identity<string>("hello");

Each call produces a fully specialised version of the function. See section 12.6 Monomorphisation.

To require capabilities of T (such as the ability to compare it with <), use a where clause:

function smaller<T>(a: T, b: T) -> T
    where T: Ord {
    return if (a.compare(&b) == Ordering::Less) { a } else { b };
}

4.3 Variadic Functions

A trailing ... marks a function as variadic. The signature mirrors C's variadic calling convention, so variadic functions stay ABI-compatible with the printf-family at the FFI boundary.

// FFI / intrinsic declarations: a bare `...` bucket, no body.
intrinsic function printf(format: string, args...) -> i32;

A user-defined variadic function names the bucket (args...). The compiler emits va_start/va_end around the body and binds args to the raw va_list pointer. Wrap it in a VaArgs (std::core::varargs, in the prelude) to read typed values without hand-rolling va_arg:

function sum(count: i32, args...) -> i64 {
    mut va: VaArgs = VaArgs::new(args);
    mut total: i64 = 0;
    for (mut i: i32 = 0; i < count; i++) {
        const v: i64 = va.next();      // ![implicit]: T inferred from `v`'s type
        total += v;
    }
    return total;
}

va.next<T>() is the explicit form; va.next() infers T from the expected type at the call site (see section 17 on ![implicit]). va.as_ptr() returns the raw va_list pointer for forwarding to a C v*printf callee - equivalently, pass the original args identifier.

Two limits are inherited from C varargs and no wrapper can remove them:

  • Not count-safe. Nothing records how many arguments were passed or their types; the callee must learn that out of band (a format string, a leading count, a sentinel).
  • Default argument promotions apply. A variadic call promotes i8/i16/boolean to i32 and f32 to f64. VaArg is therefore implemented only for the promoted scalar set (i32, u32, i64, u64, f64, string); va.next<i8>() is a compile error - read it as i32 and narrow. Pass i64-typed values when reading with next<i64>().

4.4 Extern Functions

extern declares a function whose body is provided by the linker, typically a C library symbol.

extern function exit(code: int) -> void;

extern "C" {
    function puts(s: string) -> int;
    function atoi(s: string) -> int;
}

See section 18 for full FFI semantics, including extern "C" { ... } blocks and the extern module c := "C" { #include <header.h> } form.

4.5 Intrinsic Functions

intrinsic function declares a function that the compiler lowers directly to LLVM IR rather than emitting a real call. The standard library uses intrinsics for primitives such as memory operations, formatted printing, and the panic mechanism.

intrinsic function malloc(size: u64) -> void*;
intrinsic function free(ptr: void*) -> void;
intrinsic function memcpy(dest: void*, src: void*, count: u64) -> void*;
intrinsic function strlen(str: string) -> u64;
intrinsic function printf(format: string, args...) -> i32;

The user-facing print / println / eprint / eprintln are not intrinsics - they live in std::fmt and forward to the variadic printf family. Only the raw C-shaped primitives above are intrinsics.

The complete list of intrinsics is the file stdlib/core/intrinsics.cryo. User code does not typically declare its own intrinsics; they are a contract between the standard library and the compiler.

The compiler also expands two source-location pseudo-constants at the call site:

ConstantExpands to
FILEThe current source file path (string).
LINEThe current line number (i32).

These are used by panic, assert, and the testing framework to report failure locations without the caller passing them by hand.