2Type System
Every value in Cryo has a known type at compile time. A binding's type is either written explicitly or inferred from its initialiser (local inference only - see section 3). There are no implicit conversions between numeric types; when you need a conversion, you write it with as.
2.1 Primitive Types
| Type | Description | Size |
|---|---|---|
void | No value; only valid as a return type. | 0 |
boolean | true / false. Not interchangeable with integers. | 1 byte |
char | 8-bit character (byte). | 1 byte |
string | NUL-terminated raw string (u8*). FFI-shaped. | pointer |
int | Default signed integer; alias for i32. | 4 bytes |
i8 i16 i32 i64 i128 | Signed integers of fixed width. | 1 / 2 / 4 / 8 / 16 bytes |
uint | Default unsigned integer; alias for u32. | 4 bytes |
u8 u16 u32 u64 u128 | Unsigned integers of fixed width. | 1 / 2 / 4 / 8 / 16 bytes |
float | Alias for f32 (single-precision IEEE 754). Distinct from double. Bare float literals still default to f64. | 4 bytes |
f32 f64 | IEEE 754 floats. | 4 / 8 bytes |
double | Alias for f64. | 8 bytes |
usize isize | Pointer-width unsigned / signed integers - distinct types whose width tracks the target's pointer size (the natural type for sizes and indices). | 8 bytes on 64-bit |
never | The type of an expression that does not return. Only valid as a return type, and written by functions that always panic, trap, or diverge. | 0 |
() | The unit type. A value of this type carries no information. | 0 |
In performance-sensitive or cross-platform code, prefer the explicit-width forms (i32, u64, f64) so the layout is unambiguous. The shorthand aliases exist for ergonomics.
string is a NUL-terminated raw byte pointer matching the C ABI. Inside the standard library, length-typed UTF-8 is modelled by Str (borrowed) and String (owned). The translation between them happens at the FFI boundary in ffi::cstr.
2.2 Pointer Types
A pointer holds a memory address. Pointer types are written by suffixing the pointee with *, mirroring the C convention.
import std::core::intrinsics::{ malloc };
const p: int* = &x; // pointer to int
const pp: int** = &p; // pointer to pointer
const v: void* = malloc(64); // type-erased
A pointer converts to another pointer type without a cast only when the pointee is the same type (T* to const T*, never back), or when either side is void*. A pointer to any other type is written with as, including a pointee that converts to the wanted one: an i32 converts to an i64, but an i32* addresses four bytes, so it is not an i64* (nor is an i32** an i64** or a void**). A value passed where a pointer parameter wants its address must be of the pointee type itself. The rule is the same wherever a value meets a declared type: an initializer, an assignment, a return, a struct field, and an argument of every kind of call - a method, a free or static function, a function reached by its path and a generic one (E0214 at an argument, E0200 elsewhere).
Raw pointers are unchecked. Validity, aliasing, and lifetime are the programmer's responsibility. For a pointer that is statically guaranteed non-null, see core::ptr::NonNull<T>. For owned heap allocations, prefer Box<T>. See section 14.
2.3 References
References use &. They appear principally as method receivers (&this for shared access, mut &this for exclusive, mutating access) and as function parameter types.
&int // shared reference to int
mut &int // exclusive reference to int
The receiver shape on a method is part of its signature: a &this method may not modify the receiver; a mut &this method may. Callers see this distinction without reading the body.
Reading the value a reference refers to is explicit in arithmetic and comparison: *x == 7, *x % 2. Writing x == 7 or x % 2 with x: &int is an error, never a comparison of the reference itself (section 5.2). Member access reads through the reference: h.tag with h: &Heap.
2.4 Array Types
Cryo has two array types: the fixed array [T; N], which holds its N elements in place, and the dynamic array [T], growable and heap-backed. The dynamic array is the standard library's Array<T> with the default allocator: [T] is a spelling of that type, not a second one, so a [T] and an Array<T> are interchangeable everywhere. An array with another allocator is written out, Array<T, A>; [T] never names one.
An array type is written in brackets: [T] is a dynamic array and [T; N] a fixed one of N elements. The element between the brackets is a whole type, so [&Node] is an array of references, &[Node*] a reference to an array of pointers, [(i32) -> i32] an array of functions, and [[u64; 2]; 3] three rows of [u64; 2] - the length written nearest the element belongs to the inner array. A , after the first type, [T, U], is refused (E0104): a tuple is written (T, U).
An array is never written after its element: a type followed by [ is refused (E0104, "array types are written [T]: write [i64]"), and the diagnostic carries the bracketed spelling as a fix that an editor can apply. The fix writes nested lengths outside-in, so u64[2][3] becomes [[u64; 2]; 3], and drops parentheses the brackets make unnecessary, so (&Node)[] becomes [&Node]. new u8[n] is not an array type but an allocation yielding a pointer (section 14), and keeps its spelling.
const buf: [int; 16]; // fixed: 16 ints held in place
mut v: [int]; // dynamic: `Array<int>`, growable, heap-backed
mut w: Array<int>; // the same type as `v`, written out
A fixed array does not convert to a dynamic one: const d: [int] = buf; is refused (E0200). A dynamic array has Array's methods, iterates with for as Array does, and is indexed through Array's Index implementations: xs[i] is *(xs.index(i)), for an index of any integer type, in a generic body as anywhere else. An index out of range panics, and the report names the xs[i] that wrote it (![caller_location], section 4.5).
Diagnostics and the editor spell an array the way the annotation that declared it wrote it: one written [i32] shows as [i32], one written Array<i32> as Array<i32>. The spelling is for display only. A type no annotation wrote, an inferred one, shows as [T] when its allocator is the default and as Array<T, A> otherwise.
An array literal written where an Array<T> with the default allocator is expected builds that array, in every form: [a, b], [], and [value; n] with n fixed or known only at run time. The literal allocates the storage, so an Array<T, A> with another allocator is not initialised by one. A literal passed for a [T] parameter whose T the call infers is the Array of its own element type, and its elements are what bind T: for function generic_len<T>(xs: [T]) -> i64, generic_len([true, false]) calls generic_len::<boolean>. An array literal whose elements are all numeric literals is as width-polymorphic as one numeric literal: when another argument or the expected type binds T, its elements are typed as that T instead of their defaults, so count_eq([1, 2], wide) with count_eq<T>(xs: [T], y: T) and wide: i64 binds T = i64 and passes an [i64]. An element that cannot be that T (a float literal for an integer T, an integer out of its range) is refused as a single literal would be.
An array's length and capacity, read as fields or through length() and capacity(), are i64. Array's methods take an index or an element count as a u64.
The N in [T; N] is a compile-time integer: an integer literal, a named const (declared in this module or another, written bare or qualified), an enum variant, a cast between integer types, or arithmetic, comparison, logical, and bitwise operators over those. The repeat expression [value; N] accepts exactly the same set and yields [T; N], so a field and the initializer that fills it can share one name for their length; a count that is not a compile-time integer is legal there and yields a dynamic [T] sized at run time. A size that is neither is E0239. value is evaluated once and the same bits are stored in every slot, so its type must be a plain-copy type (one that satisfies Copy); an element that releases what it owns would be released once per slot, and is refused (E0453).
const CAPACITY: i64 = 512;
type struct Grid { cells: [boolean; CAPACITY]; }
const cells: [boolean; CAPACITY] = [false; CAPACITY];
sizeof/alignof are not available in this position: an array's size is fixed before any layout is computed. static_assert runs after layouts exist and does accept them (section 17.5).
2.5 Function Types
Functions are first-class values. A function type names its parameter types and its return type.
(int, int) -> int
(T) -> U
() -> void
Function types appear most often as parameter types for higher-order combinators:
function apply(f: (int) -> int, x: int) -> int {
return f(x);
}
This is how Option::map, Result::and_then, and the iterator combinators take their callback.
A function value can be a named function or a lambda expression:
(params) -> Ret { body }. The body is always brace-delimited; each parameter
and the return type are written out explicitly.
const inc: (int) -> int = (n: int) -> int { return n + 1; };
const add: (int, int) -> int = (a: int, b: int) -> int { return a + b; };
apply(inc, 41); // 42 (named-as-value)
apply((n: int) -> int { return n * 2; }, 21); // 42 (inline)
A lambda that references a binding from the enclosing scope captures it,
becoming a closure. Copy types (i32, u64, bool, char, references, and any
aggregate whose components are all Copy and which has no Drop impl - Copy
is decided structurally, never declared) are captured by value-copy; non-Copy types are
captured by move - the outer binding is consumed at the lambda's
construction site (subsequent use is E0452) and the closure-struct's
synthesized Drop releases the captured value at scope exit. The move
keyword stays valid as an explicit prefix (move (params) -> T { body })
but is no longer required for non-Copy captures: any non-Copy capture
implicitly flips the lambda to move semantics. The compiler synthesises an
anonymous struct holding the captured fields plus a __call__ method whose
body is the lambda body; the closure value is the struct instance and the
call site dispatches directly through __call__. Stack-allocated; no heap
allocation. A closure with a non-Copy capture implements Drop - an empty
drop, after which the captures are released as any aggregate's fields are
- so a written
f.drop()releases what it captured exactly once, and so does leaving its scope; a closure whose captures are all Copy is itself Copy.
const bias: i32 = 10;
const add_bias = (x: i32) -> i32 { return x + bias; }; // captures `bias`
add_bias(32); // 42
A closure can also be passed to a (Args) -> Ret-typed parameter; the
compiler specialises the receiver function per concrete closure type so
the body still issues a direct call, never an indirect one. Named
functions and non-capturing lambdas continue to bind to the same
parameter as bare function pointers, with no overhead change:
function apply(f: (i32) -> i32, x: i32) -> i32 { return f(x); }
const bias: i32 = 10;
apply((x: i32) -> i32 { return x + bias; }, 32); // 42 - capturing closure
apply((x: i32) -> i32 { return x * 2; }, 21); // 42 - non-capturing lambda
apply(tentimes, 4); // 40 - named function pointer
Naming a function as a value. A named function used without a call is a function value. When the name is overloaded (section 4.6), the expected function type picks the overload; with no expected type the use is ambiguous (E0154), and the error's help shows the annotation that picks one:
function twice(x: i32) -> i32 { return x * 2; }
function twice(x: f64) -> f64 { return x * 2.0; }
const f: (i32) -> i32 = twice; // the `twice(i32)` overload
const g = twice; // error[E0154]: `twice` is overloaded, and nothing here says which overload is meant
// help: give the variable a function type to pick one: `const g: (i32) -> i32 = twice;`
A generic function used as a value infers its type arguments from its use, as a call infers them from its arguments. The expected function type supplies them, and the turbofish names them explicitly:
function idf<T>(x: T) -> T { return x; }
const h: (i32) -> i32 = idf; // idf::<i32>
const k = idf::<i64>; // (i64) -> i64
const n: i32 = h(3); // 3
A bare name in expression position is a value. An identifier written alone in an expression is looked up among values only. A name that declares only a type is refused there - as a value (E0201) and as a callee (E0231) - so Point(1, 2) is an error and Point { x: 1, y: 2 } is the literal.
Calling something that is not a function. A call whose callee has a non-function type is refused with E0213, whether the callee is a call's result, a local or a field:
type struct H { n: i32; }
function one() -> i32 { return 1; }
const x: i32 = 3;
const h: H = H { n: 1 };
const a: i32 = one()(3); // error[E0213]: expected function, found `i32`
const b: i32 = x(3); // error[E0213]: expected function, found `i32`
const c: i32 = h.n(4); // error[E0213]: expected function, found `i32` (`n` is a field)
Where a capturing closure may be passed
The specialisation above happens per call site, and in 1.0 it is wired for one call shape: a non-generic free function. Passing a capturing closure anywhere else is rejected at compile time with E0458 rather than silently falling back to an indirect call:
| Callee | Capturing closure | Non-capturing lambda / named fn |
|---|---|---|
Non-generic free function - apply(c, x) | yes | yes |
Generic free function - apply::<T>(c, x) | E0458 | yes |
Method - obj.run(c) | E0458 | yes |
Scope-resolution call - Type::run(c) | E0458 | yes |
extern "C" callback parameter | never (no environment slot) | yes |
A capturing closure is not a function pointer, so a slot of function type cannot hold one either. A variable, field or array element typed (Args) -> Ret, a place of that type assigned to, and a function returning one are each refused where the capturing closure meets them (E0458). A variable with no annotation takes the closure's own type and holds it:
const bias: i32 = 40;
const f = (x: i32) -> i32 { return x + bias; }; // ok: the closure's own type
const g: (i32) -> i32 = (x: i32) -> i32 { return x + bias; }; // error[E0458]: a capturing closure cannot be held by a variable of function type `(i32) -> i32`
The distinction is capture, not syntax. A lambda that captures nothing is an
ordinary function pointer and binds anywhere a function pointer does, including
every row above; a lambda that captures becomes an anonymous struct value, and
only the first row knows how to specialise a receiver for it. This is why
opt.map((n: int) -> int { return n * 10; }) is fine while the same call with
a lambda that closes over a local is E0458 - the callee is a method.
Two ways around it:
// 1. Pull the call into a free function that takes the closure.
function run_with(f: (i32) -> i32, v: i32) -> i32 { return f(v); }
run_with((x: i32) -> i32 { return x + bias; }, 32);
// 2. Close over nothing - pass the value as an argument instead.
opt.map((n: int) -> int { return n * 10; });
These are deferred capabilities, not bugs: the grammar and the closure representation already accommodate them, only the receiver-specialisation paths for generic, method, and scope-resolution callees are unimplemented.
A combinator that infers a new type parameter from the callback's return
type - for example Option::map<U> - does so automatically: U is bound from
the callback's signature, so the type argument may be omitted. This works the
same whether the callback is a lambda or a named function:
const some: Option<int> = Option::Some(5);
const out: Option<int> = some.map((n: int) -> int { return n * 10; }); // U = int, inferred
function tentimes(n: int) -> int { return n * 10; }
const out2: Option<int> = some.map(tentimes); // U = int, inferred
The explicit form is still accepted (some.map::<int>(...)) and is required only
when the type parameter appears nowhere the call can infer it from.
2.6 Tuple Types
A tuple groups a fixed number of values - of possibly different types - into one compound value. Tuple types and tuple literals both use parentheses:
type Pair = (int, string);
type Triple = (int, int, int);
const p: (int, string) = (42, "answer");
const x: int = p.0; // positional element access: .0, .1, ...
const s: string = p.1;
const y: int = p[0]; // `t[N]` indexing is equivalent to `t.N`
Because parentheses are also used for grouping, the unit type, and function types, the forms are:
()- the unit type / unit value, which also serves as the empty tuple (section 2.7).(T)/(expr)- grouping: justT/expr, not a 1-tuple.(T,)/(x,)- a 1-tuple: the trailing comma distinguishes it from grouping.(T, U),(T, U, V), ... - 2-, 3-, ... element tuples.(A, B) -> R- a function type, not a tuple (the->disambiguates).
Element access is positional with an integer literal - t.0, t.1, ... (or the
equivalent t[0], t[1]) - and the index is checked against the tuple's arity
at compile time. Chained access like t.1.0 works (element 1, then element 0 of
that).
Note: a leading
[in a type starts an array type,[T]or[T; N], so[T, U]is refused (E0104). Write(T, U).
2.7 The Unit Type
The unit type () represents "a value that carries no information." It is distinct from void:
void: a function produces no value.(): a function produces a value, but the value has zero meaningful payload.
() appears in generic positions where a type parameter is required but no data is needed. The canonical example is Result<(), Error> for an operation that either succeeds (with nothing to return) or fails with an error.
The expression () is the one value of the unit type, and it has type ():
function done() -> () { return (); }
const u: () = done();
const r: Result<(), i32> = Result::Ok(());
() is only the unit value. It is not a null pointer: written where a pointer is expected (const p: i32* = ();, an assignment, an argument), it is a type mismatch. The null pointer is null.
2.8 Type Aliases
A type alias introduces a new name for an existing type. Aliases are transparent: the alias and the original are interchangeable.
type Byte = u8;
type StringResult<T> = Result<T, string>;
type Callback = (int) -> void;
A function's parameter and return types written as aliases are the types they name, as everywhere else: a parameter cb: Callback is called as a function, and a call returning Byte takes the operators of u8. So two overloads whose parameters differ only by an alias and its target, pick(x: u8) and pick(x: Byte), have one signature, and the second is a redefinition (E0206).
A path through an alias names what the same path through its target names, type arguments included: with type PI = Pl<i32>;, PI::plain(8) is Pl::<i32>::plain(8), and with type Hue = Color;, Hue::Green is Color::Green. An alias of an alias is followed. An alias with no parameters of its own fixes its target's arguments, so arguments written on it, PI::<u8>::size(), are too many and refused (E0307); a path with other arguments is written through the target, Pl::<u8>::size(). An alias of a type that is no named type (Callback) names no path. A path through an alias with parameters of its own, StringResult::<i32>::Ok(1) or StringResult::Ok(1), is refused where it is written (E0233); the path is written through the type the alias names. So is a struct literal through one: with type Keyed<T> = Pair<T, i64>;, Keyed::<boolean> { ... } and Keyed { ... } are refused (E0233) and the literal is written Pair::<boolean, i64> { ... }, while a non-generic alias's literal, Fixed { ... } with type Fixed = Pair<boolean, i64>;, builds its target. Such an alias is a type like any other.
2.9 Casting with as
Cryo never inserts an implicit numeric or pointer conversion. To convert between types, use the as keyword. The compiler does not insert range checks for narrowing casts; this is a deliberate choice that keeps the conversion's cost visible.
const a: i64 = 42;
const b: i32 = a as i32; // narrowing: programmer's responsibility
const p: u8* = some_string as u8*; // pointer reinterpretation
A cast to a struct, tuple or array value is a cast from a value of that same type, and nothing else: p as Point with p a pointer, or one aggregate cast to another, is refused (E0208); *p reads the value a pointer points at.
A struct, tuple or array value is not cast to a pointer or a scalar either (E0208): v as void* is refused, and the value's address is written &v (and converted as &v as void*). Inside a method taking &this, this is that address already, so this as void* is allowed.
2.10 Optional Types (T?)
T? is shorthand for Option<T>. It is pure sugar - the parser rewrites T?
to Option<T>, so the two are the same type. A T? value carries all of
Option's methods (is_some, is_none, unwrap_or, map, ...) and is freely
assignable to and from Option<T>. The suffix works in every type position:
variable, parameter, return type, struct field, and generic argument.
const slot: int? = Option::Some(7); // int? is Option<int>
const back: Option<int> = slot; // interchangeable both ways
function first(xs: [int]) -> int? { // optional return
if (xs.length == 0) { return Option::None; }
return Option::Some(xs[0]);
}
type struct Config {
port: u16?; // optional field
}
T? nests like any other type argument: Option<int?> is
Option<Option<int>>.
2.11 Opaque Types (implement Trait)
implement Trait in a return type or a variable annotation is an opaque
type: it stands for one specific concrete type without naming it. The
compiler infers the real type and uses it everywhere; you simply do not have
to write it.
// The concrete iterator (`SliceIter<T>`) never appears in the signature.
iter(&this) -> implement Iterator<T> where T: Copy {
return SliceIter::<T> { ptr: this.ptr, remaining: this.length as u64 };
}
// A caller binds the result without naming the concrete type either.
mut it: implement Iterator<i32> = arr.iter();
const n: u64 = it.count(); // trait methods are available
Because Cryo monomorphises, this is a purely static, zero-cost construct: an
implement Iterator<i32> value is the underlying concrete type (here
SliceIter<i32>), method calls dispatch statically, and there is no heap
allocation, vtable, or runtime indirection. It corresponds to "return some
single type that implements this trait", not to a dynamically-dispatched
trait object. (implement is the same keyword used for implement blocks; in
type position it introduces an opaque type, while at the start of a
declaration it introduces an implement block - the two never overlap.)
Where it is allowed. Two positions:
-
Return type - the concrete type is inferred from the body's first
returnexpression. That expression must currently be a struct literal (e.g.return SliceIter::<T> { ... }), which is how every standard-library iterator is written. -
Variable binding -
mut it: implement Iterator<i32> = expr;. The type is taken from the initialiser, which is then checked to actually implement the named trait; a mismatch isE0200:mut it: implement Iterator<i32> = some_non_iterator; // E0200When the initialiser is a concrete static constructor (
mut it: implement Iterator<i32> = Range::<i32>::new(0, 10);), you can re-adapt the local directly -it.take(3)specialises the adapter against the recovered concrete receiver. The binding's visible type is still the opaque trait; the compiler recovers the concrete initialiser type for combinator specialisation.This recovery only applies when the initialiser names its concrete type. When the producer itself returns an opaque iterator (
mut it: implement Iterator<i32> = arr.iter();, whereiterreturnsimplement Iterator<...>), the concrete cursor is hidden behind that opaque return, so the local has no concrete receiver to specialise against - chain the combinator on the producing expression instead (arr.iter().take(2).count()), or bind to the concrete adapter type when you need a named local (mut z: ZipIter<Range<i32>, Range<i32>> = a.zip(b);).
To accept any iterator as a parameter, use a generic with a trait bound instead - that is the tool for the input side:
function sum<I>(mut it: I) -> i32 where I: Iterator<i32> { ... }
Multiple bounds combine with +: implement Iterator<i32> + Clone.
One concrete type per site. An opaque return names a single underlying
type - every return in the body must produce the same one. Two iterators of
different concrete types cannot be returned from one implement Trait
function (that would require a dynamically-dispatched trait object, which Cryo
does not provide).
The trait argument carries the trait's associated item: Iterator<i32> is
sugar for Iterator<Item = i32>, so a binding annotated implement Iterator<i32> is verified both to implement Iterator and to have an
actual Item of i32. A genuine element-type mismatch (e.g. binding an
iterator of String to implement Iterator<i32>) is rejected as E0200.
The cross-check is category-level: it flags a structurally distinct item
(one user struct vs another, struct vs primitive) but stays silent when the
declared and actual items are implicitly inter-convertible (i32 vs i64)
or differ only in representation (String vs String<GlobalAlloc>).
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).
intrinsic function malloc(size: u64) -> void*const 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 p: int*const pp: int**const p: int*const v: void*mut &this
mut &TypeAn exclusive, mutating borrow. As a receiver, mut &this lets the method modify the fields - and callers can see that from the signature alone. As a parameter type, mut &T lets the callee mutate through the reference while the caller keeps ownership.
const buf: [int; 16]mut 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 v: [int]mut w: Array<int>type 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::arrayconst CAPACITY: i64type 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 GridGrid.cells: [boolean; CAPACITY]const CAPACITY: i64const cells: [boolean; CAPACITY]falseThe boolean literal. Booleans are a distinct 1-byte type, not interchangeable with integers; if and while conditions must be boolean.
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 apply(f: (int) -> int, x: int) -> int(parameter) f: (int) -> int(parameter) x: 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) f: (int) -> int(parameter) x: intconst inc: (int) -> int(parameter) n: int(parameter) n: intconst add: (int, int) -> int(parameter) a: int(parameter) b: int(parameter) a: int(parameter) b: intfunction apply(f: (int) -> int, x: int) -> intconst inc: (int) -> int(parameter) n: intconst bias: i32const add_bias: i32(parameter) x: i32(parameter) x: i32const bias: i32const add_bias: i32function apply(f: (i32) -> i32, x: i32) -> i32(parameter) f: (i32) -> i32(parameter) f: (i32) -> i32(parameter) x: i32function apply(f: (i32) -> i32, x: i32) -> i32(parameter) x: i32const bias: i32(parameter) x: i32function twice(x: i32) -> i32(parameter) x: i32function twice(x: f64) -> f64(parameter) x: f64(parameter) x: f64const f: (i32) -> i32function twice(x: f64) -> f64const g: f64function idf<T>(x: T) -> Ttype parameter T(parameter) x: Ttype parameter T(parameter) x: Tconst h: (i32) -> i32function idf<T>(x: T) -> Tconst kconst n: i32const h: (i32) -> i32type struct HH.n: i32function one() -> i32const x: i32const h: Htype struct HH.n: i32const a: i32function one() -> i32const b: i32const x: i32const c: i32const h: Hconst f: i32(parameter) x: i32const bias: i32const g: (i32) -> i32(parameter) x: i32function run_with(f: (i32) -> i32, v: i32) -> i32(parameter) v: i32(parameter) f: (i32) -> i32(parameter) v: i32function run_with(f: (i32) -> i32, v: i32) -> i32(parameter) x: i32const bias: i32(parameter) n: intconst some: Option<int>Option::Some(T)const out: Option<int>const some: Option<int>Option::map<U>(&this, f: (T) -> U) -> Option<U>(parameter) n: intfunction tentimes(n: int) -> int(parameter) n: intconst out2: Option<int>function tentimes(n: int) -> inttype Name = Existing;A type alias: a new, transparent name for an existing type - the two are interchangeable everywhere. Aliases may be generic: type StringResult<T> = Result<T, string>;.
type Pair = (int, string);primitive stringNUL-terminated raw string (u8*), matching the C ABI. Length-typed text is Str / String.
type Triple = (int, int, int);const p: (int, string)const x: intconst p: (int, string)const s: stringconst y: intfunction done() -> ()primitive ()The unit type: a value that carries no information, and the empty tuple. Distinct from void, which is the absence of a value - Result<(), E> succeeds with nothing to return. 0 bytes.
const u: ()function done() -> ()const r: Result<(), i32>Result::Ok(T)type Byte = u8;type StringResult<T>type Callback = (int) -> void;const a: i64const a: i64value 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 p: u8*const slot: int?const back: Option<int>const slot: int?function first(xs: [int]) -> int?(parameter) xs: [int]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) xs: [int]Array.length: i64Option::Nonetype struct ConfigConfig.port: u16?&this
mut &this
thisThe receiver of a method. Its first parameter declares how the method takes it: &this borrows read-only, mut &this borrows exclusively, and this / mut this move the value in and consume it. In the body, this.field reaches the members.
-> implement Trait<T>An opaque type: some one concrete type that implements the trait, without naming it. The compiler infers the real type and uses it everywhere; callers see only the trait's methods. How the stdlib returns iterators without exposing their engines.
language referencetype trait Iterator {
type Item;
next(mut &this) -> Option<This::Item>;
count(mut &this) -> u64;
fold<Acc>(mut &this, initial: Acc, f: (Acc, This::Item) -> Acc) -> Acc;
for_each(mut &this, f: (This::Item) -> void) -> void;
take(this, n: u64) -> TakeIter<This>;
skip(this, n: u64) -> SkipIter<This>;
map<B>(this, f: (This::Item) -> B) -> MapIter<This, B>;
filter(this, pred: (&This::Item) -> boolean) -> FilterIter<This>;
chain<J>(this, other: J) -> ChainIter<This, J>;
// … 7 more
}where 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 Copy {}Types whose values can be duplicated by a bitwise copy. Integers, floats, pointers, and pure-POD structs qualify; anything that owns resources (heap storage, file handles, refcount) does not.
std::core::markertype struct SliceIter<T> {
ptr: T*;
remaining: u64;
}SliceIter.ptr: T*SliceIter.remaining: u64mut it: implement Iterator<i32>const n: u64mut it: implement Iterator<i32>Iterator::count(mut &this) -> u64Number of remaining elements. The default walks the iterator to count, consuming it. Implementations with O(1) knowledge of their length should override.
std::core::iterfunction sum<I>(mut it: I) -> i32
where I: Iterator<i32>type parameter Iwhere I: Iterator
(parameter) mut it: Ifunction 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.
trueThe boolean literal. Booleans are a distinct 1-byte type, not interchangeable with integers; if and while conditions must be boolean.
primitive usizePointer-width unsigned integer; the natural type for sizes and indices.
primitive typeprimitive 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 typeconst Ttype struct NonNull<T> {
ptr: T*;
}type struct Box<T, A = GlobalAlloc> where A: Allocator {
ptr: T*;
alloc: A;
}Box<T, A>: single-owner heap allocation.
std::alloc::boxconst x: intconst h: Hnew 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.
const n: u64const d: [int]const buf: [int; 16]for (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.
type trait Index<Idx, Output> {
index(&this, idx: Idx) -> Output*;
}Index<Idx, Output> is the contract for container[idx]-style access, returning a pointer to the element. The compiler rewrites a[i] into *(a.index(i)); because the deref of the returned Output* is a place, the one impl serves reads (v = a[i]), writes (a[i] = v), and compound assignment (a[i] += v).
function generic_len<T>(xs: [T]) -> i64(parameter) xs: [T](parameter) y: Tconst 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.
sizeof(T)The size of a type in bytes, as a compile-time constant - usable in static_assert. alignof(T) gives the alignment.
alignof(T)The alignment of a type in bytes, as a compile-time constant - usable in static_assert. sizeof(T) gives the size.
static_assert(sizeof(T) == 4, "message");A module-scope, compile-time assertion. The condition is folded after layouts are computed - literals, sizeof, alignof, and the arithmetic, comparison, logical, and bitwise operators - and a false or non-constant condition fails the build (E0237). Its main use is checking that a struct matches a C layout.
Result::and_then<U>(&this, op: (T) -> Result<U, E>) -> Result<U, E>move (params) -> Ret { body }Prefixes a lambda that captures its environment by move: captured outer bindings are consumed where the lambda is built and released by a synthesised Drop at scope exit. Capturing a non-Copy value already gives a lambda move semantics; move states it explicitly.
const f: i32function apply(f: (i32) -> i32, x: i32) -> i32const c: i32extern 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 referenceconst some: Option<int>const p: i32*nullThe null pointer literal, valid in any pointer context - including the primitive string, which is a NUL-terminated u8*. Compare with == null; it is not an Option.
type Callback = (int) -> void;type Byte = u8;(parameter) x: u8(parameter) x: Bytetype PI = Pl<i32>;type Hue = Color;type StringResult<T>type Keyed<T>type Pair = (int, string);type Fixed = Pair<boolean, i64>;const p: u8*const v: void*implement struct Name { ... }
implement trait Trait for Type { ... }
implement u64 { ... }Adds methods to a type outside its declaration. implement struct T (or enum, or a bare primitive) opens an inherent block; implement trait Tr for T supplies a trait's methods for one type. Resolution is at compile time - no dynamic dispatch.
type struct Range<T> {
start: T;
end: T;
}static Range::new(start: T, end: T) -> Range<T>mut z: ZipIter<Range<i32>, Range<i32>>type struct ZipIter<I, J> {
first: I;
second: J;
}Adapter pairing two iterators positionally, yielding Pair<A, O> until either side runs out. Chain-shaped: the element types A (from I) and O (from J) live only in the impl where-bounds, not as struct parameters, so the receiver type only mentions the two inner iterators.
const a: i64const b: i32type struct String<A = GlobalAlloc> {
buffer: RawBuffer<u8, A>;
length: u64;
}String: owned, heap-allocated, length-typed UTF-8.
std::collections::stringtype struct GlobalAlloc {}Zero-sized handle to the process-wide allocator. Construct with GlobalAlloc::new(); all instances dispatch through the same underlying backend.