4Functions
4.1 Function Declarations
import std::fmt::{ printf };
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 after :: - the turbofish:
const n: int = identity::<int>(42);
const s: string = identity::<string>("hello");
Each call produces a fully specialised version of the function. See section 11.6 Monomorphisation.
In expression position < opens generic arguments only immediately after ::; anywhere else it is the comparison operator. So identity::<int>(42), Array::<int>::new(), Pair::<int, int> { first: 1, second: 2 }, obj.method::<T>(x) and Scope::method::<T>(x) name a specialization, while g(LIMIT < x, MAXV > (y)) is two comparisons whatever LIMIT and MAXV are - the same text never means two things by where its names were declared. Every :: after the arguments is an ordinary path step. Type position is unaffected: Array<int> in an annotation, a where bound or a static match arm, and new Array<int>(..), are written as before, because a < after a type name is never a comparison.
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.
// An FFI declaration: a bare `...` bucket, no body.
extern "C" {
function printf(fmt: u8*, ...) -> 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 16 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/booleantoi32andf32tof64.VaArgis therefore implemented only for the promoted scalar set (i32,u32,i64,u64,f64,string);va.next::<i8>()is a compile error - read it asi32and narrow. Passi64-typed values when reading withnext::<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 17 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 heap allocation, bit operations, atomics, float operations, and the panic mechanism.
intrinsic function malloc(size: u64) -> void*;
intrinsic function free(ptr: void*) -> void;
intrinsic function popcount64(value: u64) -> u32;
intrinsic function sqrt64(value: f64) -> f64;
intrinsic function panic(message: string, loc: Location) -> never;
printf, print, println, eprint and eprintln are not intrinsics - they are ordinary functions in std::fmt. C library functions such as memcpy and strlen are extern "C" declarations in std::ffi::libc.
The intrinsics module, std::core::intrinsics, is not in the prelude. Reach an intrinsic by path after importing the module (import std::core::intrinsics;, then intrinsics::malloc(64)), or import it by name (import std::core::intrinsics::{ malloc, free };). The complete list 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.
A place in the source is the value of the intrinsic function location(), a Location (both declared in std::core::intrinsics). panic, assert and the testing framework use it to report where a failure happened without the caller passing the place by hand:
type struct Location {
file: string; // the source file, as the build was given it
line: u32; // from 1
column: u32; // from 1
function: string; // the readable qualified name of the enclosing function
}
Each call answers the place it is written: the line and column of the call itself, and the function it is written in, named by its qualified path (App::Main::main, App::Main::Probe::own), never by its linker symbol. Two calls on one line answer two columns. Code the compiler moves into a function of its own is named for the function it is written in: a lambda's or a closure's body names the enclosing function, an async function's body the async function, and every specialization of a generic function or method the generic as written (App::Main::Holder::at).
import std::fmt;
import std::core::intrinsics;
function main() -> i32 {
const here: intrinsics::Location = intrinsics::location();
fmt::println(f"{here.file}:{here.line}:{here.column} in {here.function}");
return 0;
}
Inside a function marked ![caller_location], location() is instead the place of the call to that function: every call passes the address of its own Location as a hidden trailing argument, and a call made from inside another ![caller_location] function passes on the one it was given, so a chain of them reports the first call outside the chain - its file, line, column and function. A lambda written in such a body is a function of its own and not ![caller_location]: a place made in it is its own. The standard library's index bounds check is marked this way, so an out-of-range xs[i] reports the line of the xs[i]. Such a function cannot be taken as a value (E0151), by its name, its path, or a static method's path: a call through the value would have no location to pass. The type check refuses it, so cryo check and the editor report it. The directive is refused on a variadic function or method (E0151): the hidden argument follows the written ones, and a variadic call's written arguments have no fixed end.
![caller_location]
function expect_positive(n: i64) -> i64 {
if (n <= 0) {
core::panic("expected a positive number", intrinsics::location());
}
return n;
}
function main() -> i32 {
expect_positive(-1); // panics reporting this line
return 0;
}
panic takes the place as a Location, core::panic(message, intrinsics::location()), and a panic's report names all of it: panicked at <file>:<line>:<column> in <function>: <message>, here the file, line and column of expect_positive(-1) and the qualified name of main. The checks the compiler inserts (division by zero, a match with no arm for its value, an out-of-range index) report their place the same way. A place with no column leaves it out, and one with no function leaves out in ....
4.6 Overloading
A function name may be declared more than once with different parameter types. A call selects the one overload its arguments fit; there is no ranking among overloads that fit equally, and no implicit conversion makes an argument fit.
function conv(x: i32) -> i32 { return 32; }
function conv(x: i64) -> i32 { return 64; }
function conv(x: f64) -> i32 { return 2; }
const a: i32 = conv(1); // conv(i32): an integer literal is i32 here
const b: i32 = conv(1 as i64); // conv(i64)
const c: i32 = conv(1.5); // conv(f64): a float literal is f64 here
const s: i16 = 1 as i16;
const d: i32 = conv(s); // error[E0214]: no overload of `conv` accepts these arguments
An untyped literal argument takes one fixed type where the overloads differ in that parameter: an integer literal is i32 and a float literal is f64, the same defaults a literal takes anywhere else. Write the type you mean with as (conv(1 as i64)). A qualified call (Json::conv(1)) selects among that module's overloads the same way.
A non-generic overload does not win over a generic one by being non-generic. A call both accept is ambiguous (E0154); a generic function whose where clause the arguments fail does not accept them. A written type argument names the generic function:
function same<U>(u: U) -> i32 { return 1; }
function same(u: i32) -> i32 { return 2; }
const x: i32 = 7;
const a: i32 = same(x); // error[E0154]: `same` is ambiguous
const b: i32 = same::<i32>(x); // the generic function
An overloaded name used as a value without a call is picked by its expected function type (section 2.5).
import a::b;
import a::b::{ X, Y };
import a::b as c;
import a::b::*;Brings a module, or selected items from it, into scope. One path per declaration: use the brace form for several items, as to alias, and * for everything public (prefer the brace form; wildcards invite name collisions).
function printf(fmt: string, args...) -> i32Write a printf-style formatted string to stdout. Returns the number of bytes written, or a negative value on error (libc printf convention). Use this for genuinely variadic %s/%d output; for value formatting prefer an f-string through println (Display-based) below.
function name<T>(param: Type) -> Ret { ... }Declares a function. Parameters are written name: Type and are never inferred; the return type follows -> and defaults to void. Declaration order does not matter: every signature is collected before any body is checked, so functions may call each other freely and recurse.
function add(a: int, b: int) -> int(parameter) a: int(parameter) b: intreturn value;Leaves the current function with the given value. A function whose return type is not void must return one; a bare return; is for void. In an async function it completes the future with the value.
(parameter) a: int(parameter) b: intfunction greet(name: string) -> void(parameter) name: stringprimitive stringNUL-terminated raw string (u8*), matching the C ABI. Length-typed text is Str / String.
(parameter) name: stringfunction main() -> intfunction greet(name: string) -> voidfunction identity<T>(x: T) -> Ttype parameter T(parameter) x: Ttype parameter T(parameter) x: Tfunction swap<T>(a: T*, b: T*) -> void(parameter) a: T*type parameter T(parameter) b: T*const name: Type = value;An immutable binding: it cannot be reassigned after initialisation. The annotation may be omitted when an initialiser is present - the binding takes the initialiser's concrete type, inferred locally. Mutability is opt-in through mut.
const temp: T(parameter) a: T*(parameter) b: T*const temp: Tconst NAME: Type = value;At module level, a true compile-time constant, conventionally SCREAMING_SNAKE_CASE. (mut at module level declares mutable global state instead.)
const n: intfunction identity<T>(x: T) -> Tconst s: stringfunction smaller<T>(a: T, b: T) -> T
where T: Ordtype parameter Twhere T: Ord
(parameter) a: T(parameter) b: Twhere T: Ord + Clone, U: HashConstrains generic parameters to types that implement the listed traits, which is what makes those traits' methods callable on T. Bounds on one parameter join with +; parameters separate with ,. A where on a single method narrows the type's parameter for that method only.
type trait Ord : Eq {
compare(&this, other: &This) -> Ordering;
}Types that have a total order: every pair is Less, Equal, or Greater. compare must be consistent with equals: if a.equals(b) then a.compare(b) == Equal.
if (condition) { ... } else if (condition) { ... } else { ... }Branches on a parenthesised boolean condition. Bodies are always braced; there is no single-statement form. Used as an expression, both arms are required and must produce the same type: const sign = if (n < 0) { -1 } else { 1 };.
(parameter) a: TOrd::compare(&this, other: &This) -> Ordering(parameter) b: Ttype enum Ordering {
Less;
Equal;
Greater;
}Ordering::Lessif (condition) { ... } else { ... }The branch taken when every preceding if condition is false; else if chains another condition. In expression form the else arm is mandatory, so the expression always has a value.
extern function exit(code: i32) -> void;
extern "C" {
function puts(s: string) -> i32;
}Declares a function whose body the linker supplies - typically a C library symbol. The compiler trusts the Cryo signature; keeping it in step with the real C signature is on you.
language referencefunction printf(fmt: u8*, ...) -> i32(parameter) fmt: u8*function name(count: i32, args...)A variadic parameter: the trailing bucket receives any number of further arguments with C's calling convention, so the function stays ABI-compatible with the printf family. Wrap the bucket in VaArgs to read typed values from it.
function sum(count: i32, args...) -> i64(parameter) count: i32(parameter) argsmut name: Type = value;A mutable binding, reassignable after declaration - mutability is opt-in, const is the default. At module level it declares mutable global state. Before & it marks an exclusive reference or receiver instead.
mut va: VaArgstype struct VaArgs {
ptr: void*;
}Single-pointer wrapper over the active va_list. ptr is the raw va_list pointer - the args identifier the compiler binds in a variadic body.
static VaArgs::new(va: void*) -> VaArgsWrap a variadic body's args bucket: VaArgs::new(args). va is the raw va_list pointer the compiler binds for the args... parameter.
(parameter) argsmut total: i64for (mut i: i32 = 0; i < n; i++) { ... }
for (item in iterable) { ... }Two forms. The C-style for (init; condition; step) scopes its loop variable to the body. for (x in expr) iterates anything with next() -> Option<T>: an iterator, a range such as 0..n, an Array or Slice (their iter() is inserted for you), or a fixed-size array.
mut i: i32mut i: i32(parameter) count: i32const v: i64mut va: VaArgsVaArgs::next<T>(&this) -> T
where T: VaArgRead the next variadic argument as T (one of the promoted scalar types), advancing the list. ![implicit] lets the type be recovered from the call's expected type, so const n: i64 = va.next(); works; va.next<i64>() is the explicit form.
mut total: i64const v: i64extern function exit(code: int) -> void(parameter) code: intfunction puts(s: string) -> int(parameter) s: stringfunction atoi(s: string) -> intintrinsic function name(param: Type) -> Ret;Declares a function the compiler lowers directly to LLVM IR rather than calling. Intrinsics are the contract between the standard library and the compiler (malloc, memcpy, printf, ...); user code does not normally declare its own.
intrinsic function malloc(size: u64) -> void*(parameter) size: u64intrinsic function free(ptr: void*) -> void(parameter) ptr: void*intrinsic function popcount64(value: u64) -> u32(parameter) value: u64intrinsic function sqrt64(value: f64) -> f64(parameter) value: f64intrinsic function panic(message: string, loc: Location) -> never(parameter) message: string(parameter) loc: Locationtype struct Location {
file: string;
line: u32;
column: u32;
function: string;
}A place in the source: the file as the build was given it, the line and column (both from 1), and the readable qualified name of the function the place is in. Made by the compiler, for location() and for the caller of a ![caller_location] function.
primitive neverThe type of an expression that does not return. Only valid as a return type, written by functions that always panic, trap, or diverge. 0 bytes.
primitive typetype struct Name { ... }
type enum Name { ... }
type trait Name { ... }
type union Name { ... }Begins a type declaration; the keyword that follows says which kind. On its own, type Name = Existing; declares an alias.
type struct Name {
field: Type;
method(&this) -> Ret { ... }
}A value type with named fields and optional methods. Structs live on the stack and are passed by value. Fields are public by default and may carry = default values; methods take their receiver as &this, mut &this, or this.
type struct LocationLocation.file: stringLocation.line: u32Location.column: u32function main() -> i32const here: intrinsics::Locationtype struct Locationintrinsic function location() -> Locationconst here: intrinsics::LocationLocation.file: stringLocation.line: u32Location.column: u32function expect_positive(n: i64) -> i64(parameter) n: i64(parameter) n: i64function panic(message: string, loc: intrinsics::Location) -> neverPanic with message, reporting the place loc: intrinsics::location() written at the call, or in a ![caller_location] function its caller's. The helpers below do that automatically.
function expect_positive(n: i64) -> i64function conv(x: i32) -> i32(parameter) x: i32function conv(x: i64) -> i32(parameter) x: i64function conv(x: f64) -> i32(parameter) x: f64const a: i32function conv(x: f64) -> i32const b: i32value as TypeExplicit conversion: between numeric types, or reinterpreting one pointer type as another. Cryo never converts implicitly, and as inserts no range checks - a narrowing cast is the programmer's responsibility.
const c: i32const s: i16const d: i32const s: i16function same<U>(u: U) -> i32type parameter U(parameter) u: Utype parameter Ufunction same(u: i32) -> i32(parameter) u: i32const x: i32function same(u: i32) -> i32const x: i32type struct Array<T, A = GlobalAlloc> where A: Allocator {
ptr: T*;
length: i64;
capacity: i64;
alloc: A;
}Array<T, A>: growable, heap-backed, contiguous sequence.
std::collections::arraystatic Array::new() -> Array<T, GlobalAlloc>Empty array backed by GlobalAlloc.
type struct Pair<A, B> {
first: A;
second: B;
}Pair<A, B>: an owned two-element tuple.
std::collections::pairPair.first: APair.second: Bconst x: i32type struct Scope {
tids: [u64];
stack_bytes: u64;
}A thread scope. Every thread spawned through it is guaranteed to be joined before the scope is torn down (explicitly via join_all() or automatically when the Scope drops), so scoped bodies may safely borrow data that outlives the scope - pass a pointer to it through the body's context:
static match (T) { ... }Opens a compile-time match on a type parameter: the one arm whose type matches is kept during monomorphisation and the rest are discarded.
static match (T) {
u8 | u16 => { ... }
_ => { ... }
}Selects a body by inspecting a type parameter at compile time. The subject is a type and each arm is a type (| groups several; _ takes the rest). During monomorphisation the one matching arm is kept and the others discarded, so there is no runtime branch.
new T { field: value }
new E::Variant(args)
new T[n]Heap allocation. new T { .. } places a struct literal on the heap and yields a T*, and new E::V(args) does the same for an enum variant; new T[n] reserves n uninitialised T - the typed malloc. The storage comes from T's Allocate implementation, or else the global allocator. Parentheses after a struct's name are refused (E0117): there are no constructors, and a struct's static new(..) is an ordinary method.
start..endA half-open range - start up to but excluding end - and sugar for Range::new(start, end): an iterator usable in for, and an ordinary value anywhere else. It binds looser than arithmetic, so a..b + 1 is a..(b + 1).
function printf(fmt: u8*, ...) -> i32namespace std::core::varargs;VaArgs: a safe, typed wrapper over a C-ABI variadic argument list.
module pagetype trait VaArg {
static __pluck(p: void*) -> This;
}A type that can be read as the next variadic argument. Implemented for the C-promoted scalar set only (see the module note on argument promotion).
std::core::varargspublic module collections::array;In a directory's _module.cryo aggregator, lists a submodule and whether it is public. Importing the directory's namespace sees only the modules declared public here.
intrinsic function malloc(size: u64) -> void*intrinsic function free(ptr: void*) -> voidintrinsic function panic(message: string, loc: Location) -> neverfunction assert(condition: boolean, message: string) -> voidPanic if condition is false. Always checked.
async function name(param: Type) -> T { ... }Compiles the function into a stackless state machine. Calling it builds a future whose output is the declared return type; nothing runs until that future is polled. Allowed on free functions, methods, and trait methods.
language referencefunction at(arr: &Array<i32>, index: u64) -> i32Array element read without the Option ceremony; callers stay in range.
for (item in iterable) { ... }Separates the loop variable from the sequence in a for loop. The sequence is evaluated once; the loop lowers to loop { match (it.next()) { Some(x) => .., None => break } }.
function expect_positive(n: i64) -> i64function main() -> i32match (subject) {
Pattern(x) => { ... }
_ => { ... }
}The primary discriminator: branches on enum variants, integers, ranges, and destructuring patterns, and the compiler rejects a match that misses a case. There is no fallthrough. As an expression it evaluates to the chosen arm's value, so every arm must produce the same type.
function conv(x: f64) -> i32