10Traits
A trait names a set of methods that a type may implement. Traits are how generic code expresses requirements on its type parameters and how the standard library models capabilities such as equality, ordering, hashing, formatting, and I/O.
10.1 Declaring a Trait
type trait Eq {
equals(&this, other: &This) -> boolean;
}
Inside the trait body, This refers to the implementing type. Methods may have default bodies:
type trait Read {
read(mut &this, buf: u8*, len: u64) -> Result<u64, IoError>;
/// Default: keep reading until end-of-stream or error.
read_all(mut &this, out: mut &Array<u8>) -> Result<u64, IoError> {
// ... default implementation calls self.read in a loop ...
}
}
A trait may inherit from a base trait; implementations of the derived trait must also implement the base:
type trait Ord : Eq {
compare(&this, other: &This) -> Ordering;
}
An implementation of the derived trait for a type that does not implement the base is refused where the implement block is written (E0312: "Square does not implement Eq, which Ord requires, so Square cannot implement Ord"). A base written with arguments, type trait Sized<U> : Measure<U>, must be implemented at them, with the derived implementation's arguments put in: Sized<i64> for a type needs Measure<i64> for it. A generic implementation, implement<T> trait Ord for Box<T>, needs an implementation of the base for the same generic type, whose where-clause is not compared with the derived one's.
A trait is not a type. Written where a value's type goes - p: Shape, a return type, a field, a local, a type argument, an alias's target - it is refused where it is written (E0313), and so are &Shape and Shape* with Shape a trait. Code that accepts any type implementing a trait takes a bounded type parameter instead (section 10.3), or a trait object (section 10.7):
function area_of(p: Shape) -> i32 { ... } // error[E0313]: `Shape` is a trait, not a type
function area_of(p: &Shape) -> i32 { ... } // error[E0313]: `Shape` is a trait, so `&Shape` is not a type
function area_of<T>(p: &T) -> i32 where T: Shape { ... } // ok: one copy per implementing type
function area_of(p: virtual &Shape) -> i32 { ... } // a trait object: one copy for every type
&This inside a trait names the implementing type and is not a trait object.
10.2 Implementing a Trait
implement trait <Trait> for <Type> { ... } provides the method bodies for a concrete type.
implement trait Eq for i32 {
equals(&this, other: &i32) -> boolean {
return this == *other;
}
}
implement trait Ord for i32 {
compare(&this, other: &i32) -> Ordering {
if (this < *other) { return Ordering::Less; }
if (this > *other) { return Ordering::Greater; }
return Ordering::Equal;
}
}
You may implement a trait for any type a program can name, including primitive types.
An implementation's methods are the trait's methods, each with the signature the trait declares, read with This as the type implemented for (written as This, or as the type itself over the head's own parameters, Wrap<T> in implement<T> trait Tr for Wrap<T>), the trait's parameters as the head's arguments, its associated types as the implementation binds them, and a method's own type parameters by position, whatever they are named. Every disagreement is refused at the method, or at the head for a missing one (E0312):
- a method the trait declares without a body is missing; one the trait gives a body may be left out, and the trait's body is used;
- a method the trait does not declare;
- a static method written with a receiver, or the reverse;
- a receiver of another form (
this,&this,mut &this); - a different number of the method's own type parameters, or of its parameters;
- a parameter of another type, or another return type.
type trait Shape {
area(&this) -> i32;
scale(mut &this, k: i32) -> void;
}
implement trait Shape for struct Sq {
area(&this) -> i64 { ... } // error[E0312]: `area` returns `i32` in trait `Shape`, but `i64` here
scale(&this, k: i32) -> void { ... } // error[E0312]: `scale` takes `mut &this` in trait `Shape`, but `&this` here
}
A trait has at most one implementation for any type, and that is decided by the heads alone. Two implementations of one trait whose heads unify - some type both would accept - are refused at the later one (E0308), whether the heads are identical, one generic and one concrete (Show for Wrap<T> beside Show for Wrap<i32>: a Wrap<i32> would select both), or told apart only by their where clauses (Emit for Holder<T> where T: Alpha beside Emit for Holder<T> where T: Beta: a bound narrows which types a head is for but nothing can prove two bounds disjoint). There is no specialisation and no selection by declaration order. Heads that no type satisfies together are distinct implementations: Show for Pair<T, T> beside Show for Pair<i32, u8>, or From<i8> for u64 beside From<u8> for u64.
10.3 Trait Bounds with where
A generic parameter is constrained to types that implement specific traits via a where clause:
function smallest<T>(xs: &Array<T>) -> Option<T>
where T: Ord + Copy {
if (xs.length() == 0) { return Option::None; }
mut best: T = xs.get(0).unwrap();
for (mut i: u64 = 1; i < (xs.length() as u64); i++) {
const next: T = xs.get(i).unwrap();
if (next.compare(&best) == Ordering::Less) {
best = next;
}
}
return Option::Some(best);
}
Multiple bounds on the same parameter are joined with +. Multiple constrained parameters are separated with ,:
where T: Hash + Eq, V: Clone
A where clause is the only way to constrain a parameter; there is no inline <T: Bound> form. It is written on functions and methods, on implement blocks, and on type declarations — a struct, union or enum — after the parameter list and before the body:
type struct Holder<T> where T: Tagged {
inner: T;
label(&this) -> i32 { return this.inner.tag(); } // reached through the bound
}
type enum Slot<T> where T: Tagged {
Empty;
Full(T);
}
Every trait a bound or a supertrait names must be one in scope where it is written; a name that finds none is refused there (E0242, "cannot find trait X in this scope"), whether or not anything calls through the bound.
type trait Sub : Missing { go(&this) -> i32; } // error[E0242]: cannot find trait `Missing` in this scope
function f<T>(x: &T) -> i32 where T: Nowhere { return 1; } // error[E0242]: cannot find trait `Nowhere` in this scope
A type's bound is checked once per instantiation: Holder<Plain> with a Plain that does not implement Tagged is an error (E0306) at the annotation that wrote it, with the bound as declared beside it. An instantiation reached without an annotation — a literal, a static call — is refused the same way where it is specialised.
Inside the body that declares it, a parameter has only the methods its bounds give it. A method call on a parameter that no bound in scope names — the function's where clause, the enclosing implement block's, or the type declaration's — is an error (E0358) where the call is written, whether or not anything instantiates the body.
type struct Sink<W> {
w: W;
total(&this) -> u64 { return this.w.count(); } // error[E0358]: no method named `count` found on type `W`
}
The same holds for operators and fields. An operator applies to a parameter only when a bound licenses it - the language's own trait for that operator (Add for +, Ord for <, Eq for ==; see section 10.6) - and a bound never gives a parameter fields, whatever the type it is later instantiated with declares. Both are refused on the generic body, whether or not anything instantiates it:
function add<T>(a: T, b: T) -> T { return a + b; } // error[E0229]: the `+` operator cannot be applied to `T`: its bounds do not give it `std::core::ops::Add`
function less<T>(a: T, b: T) -> boolean where T: Ord { return a < b; } // fine: `Ord` licenses `<`
type struct Cell<T, A> { v: T; tag: A; }
implement struct Cell<T, A> {
tagged(&this) -> i32 { return this.tag.n; } // error[E0204]: no field `n` on type `A`
tag_ref(&this) -> &A { return &this.tag; } // fine; read `.n` where `A` is a concrete type
}
A user trait that is merely named Add does not license +; the bound must name the language's trait (std::core::ops::Add, in the prelude).
Inside the body, a parameter is one type: a binding, a return, an assignment, a branch or a call's argument that expects T takes a value of type T and nothing else, whatever T is later instantiated with. Array::get answers Option<T>, so it is matched or unwrapped before it is used as a T. A method whose own where clause constrains the owner's parameter (get requires T: Copy) is callable on the body's Array<T> only when the body's bounds give T that trait. Inside a static match (T) arm, T is the arm's type, and that includes the declared return type and a field declared T:
function first<T>(xs: &Array<T>) -> T where T: Copy {
const v: T = xs.get(0); // error[E0200]: expected `T`, found `Option<T>`
return v;
}
function push_first<T>(xs: mut &Array<T>) -> void where T: Copy {
xs.push(xs.get(0)); // error[E0214]: expected `T`, found `Option<T>`
}
function first_of<T>(xs: &Array<T>) -> Option<T> where T: Clone {
return xs.get(0); // error[E0358]: the trait bound `T: Copy` is not satisfied
}
function halve<T>(x: T) -> T {
static match (T) {
f64 => { const h: f64 = x / 2.0; return h; } // fine: in this arm `T` is `f64`
_ => { return x; }
}
}
10.4 Standard Library Traits
| Trait | Purpose |
|---|---|
Copy | Marker for types that are bitwise-copyable (no Drop impl). |
Drop | Explicit destructor; types implementing it are non-Copy. |
Clone | Explicit deep duplication via clone(). |
Default | A canonical zero value: static default() -> This. |
Eq | Equality (backs == / !=; see section 10.6). |
Ord (: Eq) | Total ordering via compare(...) -> Ordering (backs < > <= >=). |
Hash | Type-level hashing into a Hasher. |
Add/Sub/Mul/Div/Rem, Neg, BitAnd/BitOr/BitXor/Shl/Shr, Not/BitNot, Index, Deref | Operator overloading - see section 10.6. |
Iterator | Lazy sequence with an associated Item and next() -> Option<Item> (see section 10.5). |
IntoIterator | Conversion into an iterator. |
From<T> / Into<T> | Infallible conversions. |
TryFrom<T> / TryInto<T> | Fallible conversions returning Result. |
Read / Write | Byte-level I/O. |
Display / Debug | Formatting. |
FmtWrite | Sink trait for the formatter. |
Allocator | Heap allocation strategy. |
Every standard-library trait is declared in stdlib/core/ with the exception of Read/Write (in stdlib/io/traits.cryo), Display/Debug/FmtWrite (in stdlib/fmt/), and Allocator (in stdlib/alloc/allocator.cryo).
The collection iterator entry points (Array::iter, HashMap::keys/values, Str::split, ...) return implement Iterator<...> rather than naming their concrete cursor structs, so you consume them through the trait and a for-in loop without ever spelling the underlying type.
10.5 Associated Types
A trait may declare an associated type - a type that each implementation
supplies, named once in the trait and referred to by every method. The
standard Iterator is written this way:
type trait Iterator {
type Item; // each impl chooses its element type
next(mut &this) -> Option<This::Item>; // `This::Item` projects it
// ... defaults (count, fold, map, filter, ...) all in terms of This::Item ...
}
This::Item is a projection: inside the trait it stands for "the Item
this implementation bound". Projections also work off a generic parameter - a
generic adapter names its source's element as I::Item:
type struct MapIter<I, O> where I: Iterator { inner: I; f: (I::Item) -> O; }
A projection is answered by the parameter's bounds, so I::Item needs a bound on I whose trait declares Item. Without one, or when no bound's trait declares the name, the projection is refused where it is written (E0203, "associated type Item not found for I"). A bound on a trait that inherits Iterator answers too: the projection resolves through the bound trait's supertraits.
type trait DoubleEnded : Iterator { next_back(mut &this) -> Option<This::Item>; }
function first<I>(it: mut &I) -> Option<I::Item> where I: DoubleEnded { // Iterator's `Item`
return it.next();
}
type struct Loose<I> { f: () -> I::Item; } // error[E0203]: associated type `Item` not found for `I`
Binding the associated type. An implementation binds each associated type in one of two ways:
// 1. Positional sugar - `Iterator<i32>` == `Iterator<Item = i32>`. Available
// only when the trait has no generic params of its own (Iterator's case).
implement trait Iterator<i32> for struct Counter { ... }
// 2. Explicit body form - always available, and required when a value
// expression rather than sugar is clearer.
implement trait Iterator for struct Counter {
type Item = i32;
...
}
The positional form scales to where-clause adapters, whose element flows from
the source: implement<I, A> trait Iterator<A> for struct TakeIter<I> where I: Iterator<A> binds Item := A := I::Item.
Declaration-site bounds. An associated type may carry a bound
(type Item: Copy;). Every impl's concrete binding is checked against it - an
impl whose Item does not satisfy the bound is rejected with E0306:
type trait Seq { type Item: Copy; next_one(&this) -> i32; }
implement trait Seq<NotCopy> for struct Holder { ... } // E0306: Item not Copy
Diagnostics. Three errors guard the rules:
E0306- an impl binds anItemthat does not satisfy a declaration-site bound (type Item: Copy;).E0309- an impl of a trait that declares an associated type binds none of them (no positional arg and notype Item = ...;body). The projection could never reduce, so it is rejected up front.E0310- an associated type bound positionally on a trait that also has generic parameters. Positional args fill the declared generic params in order, so an associated type of such a trait must be bound with the explicit body form (type Out = ...;).
Because Cryo monomorphises, a projection is fully resolved at compile time:
MapIter<Range<i32>, i64>::Item reduces to i64 with no runtime cost.
10.6 Operator Overloading
Operators on a user-defined type desugar, in the compiler, to a call to the corresponding operator trait method. Implement the trait and the operator works on your type; the rewrite happens during semantic analysis, so there is no runtime dispatch and the result monomorphises like any other method call.
The rewrite is type-directed and LHS-driven: a OP b dispatches on the
type of a, and it only fires when the built-in rule does not already apply.
Primitive arithmetic (1 + 2), pointer stepping, and native integer/pointer
comparison keep emitting raw instructions - primitives deliberately do not
implement the arithmetic traits. The operator traits live in
stdlib/core/ops.cryo (Eq/Ord are in
stdlib/core/cmp.cryo).
| Operator(s) | Trait (core::ops / core::cmp) | Method / desugar |
|---|---|---|
+ - * / % | Add Sub Mul Div Rem | a + b -> a.add(&b) (etc.) |
- (unary) | Neg | -a -> a.neg() |
& | ^ << >> | BitAnd BitOr BitXor Shl Shr | a & b -> a.bitand(&b) (etc.) |
! ~ (unary) | Not BitNot | !a -> a.not(), ~a -> a.bitnot() |
== != | Eq | a == b -> a.equals(&b), a != b -> !a.equals(&b) |
< > <= >= | Ord (: Eq) | a < b -> a.compare(&b).is_lt() (is_gt/is_le/is_ge) |
a[i] | Index<Idx, Output> | a[i] -> *(a.index(i)) |
*a (deref) | Deref<Target> | *a -> *(a.deref()) |
Each arithmetic/bitwise trait carries Rhs and Output type parameters, so an
operator can mix types (add a scalar to a vector, shift by a plain integer) and
choose its result type. The right operand is taken by reference (rhs: &Rhs)
to avoid moving an owned aggregate.
type struct Vec2 { x: i64; y: i64; }
implement trait Add<Vec2, Vec2> for struct Vec2 {
add(&this, rhs: &Vec2) -> Vec2 { return Vec2 { x: this.x + rhs.x, y: this.y + rhs.y }; }
}
implement trait Mul<i64, Vec2> for struct Vec2 { // scalar on the right
mul(&this, rhs: &i64) -> Vec2 { return Vec2 { x: this.x * *rhs, y: this.y * *rhs }; }
}
implement trait Neg<Vec2> for struct Vec2 {
neg(&this) -> Vec2 { return Vec2 { x: -this.x, y: -this.y }; }
}
const a: Vec2 = Vec2 { x: 1, y: 2 };
const b: Vec2 = Vec2 { x: 3, y: 4 };
const c: Vec2 = a + b; // Vec2 { 4, 6 }
const d: Vec2 = a * 3; // Vec2 { 3, 6 }
const e: Vec2 = -a; // Vec2 { -1, -2 }
Equality and ordering. Implement Eq for ==/!= and Ord (which extends
Eq) for < > <= >=. A single compare backs all four relational operators
through the Ordering predicates:
implement trait Eq for struct Vec2 {
equals(&this, other: &Vec2) -> boolean { return this.x == other.x && this.y == other.y; }
}
implement trait Ord for struct Vec2 {
compare(&this, other: &Vec2) -> Ordering {
if (this.x != other.x) { return this.x.compare(&other.x); }
return this.y.compare(&other.y);
}
}
const eq: boolean = a == b; // a.equals(&b) -> false
const lt: boolean = a < b; // a.compare(&b).is_lt() -> true
Compound assignment routes through the same trait: a += b evaluates
a.add(&b) and stores the result back into a. This holds for every binary
arithmetic, bitwise, and shift operator (+= -= *= /= %= &= |= ^= <<= >>=).
Indexing. Index<Idx, Output>::index returns a pointer (Output*), so
the desugar a[i] -> *(a.index(i)) is a place: one implementation serves
reads (v = a[i]), writes (a[i] = v), and compound assignment (a[i] += v).
Fixed-array, string and pointer indexing keep their native path. A type may
implement Index once per index type; the call is answered by the
implementation whose Idx is the index's own type. Array<T> and [T] are
indexed this way: the standard library implements Index for Array<T> once
per integer type, marked ![inline], so xs[i] takes an index of any integer
type, costs no call, and an index at or past the length (a negative one
included) panics. In a generic body, a container whose type
carries the body's parameters (xs: &Array<T>) is indexed through its own
implementation exactly as a concrete one is.
type struct Grid { cells: [i32; 9]; }
implement trait Index<u64, i32> for struct Grid {
index(&this, i: u64) -> i32* { return &this.cells[i]; }
}
mut g: Grid = ...;
g[0] = 42; // *(g.index(0)) = 42
g[0] += 1; // index called once
Dereference and auto-deref. Deref<Target>::deref also returns a pointer
(Target*), so *b -> *(b.deref()) is likewise a place (read/write/compound).
Member access coerces through Deref too: when a receiver lacks a field or
method but implements Deref, b.field / b.method() retry on the pointee
(transitively), inserting the .deref() chain for you - the smart-pointer
pattern.
type struct Boxed<T> { value: T; }
implement<T> trait Deref<T> for struct Boxed<T> {
deref(&this) -> T* { return &this.value; }
}
mut b: Boxed<Vec2> = Boxed::<Vec2> { value: Vec2 { x: 1, y: 2 } };
const v: Vec2 = *b; // *(b.deref())
b.x = 10; // auto-deref: (*b.deref()).x - b has no field `x`
Reference operands. A by-reference operand (&T, the common
container/parameter case) overloads exactly like a value - v[i], a + b,
-a, and a == b all work when a/v is a &T. In particular &x == &y
(where the referent implements Eq) compares by value (x.equals(&y)),
not by pointer identity. A raw pointer T* is never unwrapped this way: p == null and pointer arithmetic stay on the primitive path.
Reflected (primitive left operand). When the left operand is a primitive and
the right is a user type, the operator dispatches to a trait implemented on
the primitive - 2 * v calls (2).mul(&v) given implement trait Mul<Vec2, Vec2> for i64. This is the one case where the right operand's type pulls in the
impl; Eq/Ord are excluded (their operands share This).
In generic code an operator on a type parameter is licensed by its bounds
alone, checked once on the generic body rather than per instantiation: under
where T: Add<T, T>, a + b is Add::add, and with no such bound it is refused
(E0229) even if every type the body is instantiated with implements Add (see
section 10.3). The desugar is carried through
monomorphisation, so Container<i32>::index (etc.) specialises correctly. On a
concrete type that lacks the required impl, the compiler names the missing trait.
function sum<T>(a: T, b: T) -> T where T: Add<T, T> { return a + b; }
const v: Vec2 = sum::<Vec2>(a, b); // Vec2 { 4, 6 }
10.7 Trait Objects
virtual &Shape, virtual mut &Shape and virtual Shape* are trait objects: a value of any type implementing Shape, held with a table of that type's methods for the trait. Code taking one is compiled once and serves every implementing type, where a bounded type parameter (section 10.3) is compiled once per type.
type trait Shape { area(&this) -> i32; }
type struct Square { side: i32; }
type struct Rect { w: i32; h: i32; }
implement trait Shape for struct Square { area(&this) -> i32 { return this.side * this.side; } }
implement trait Shape for struct Rect { area(&this) -> i32 { return this.w * this.h; } }
function total(shapes: [virtual &Shape]) -> i32 {
mut sum: i32 = 0;
for (s in shapes) { sum = sum + s.area(); } // each call goes through the value's table
return sum;
}
const sq: Square = Square { side: 2 };
const r: Rect = Rect { w: 2, h: 3 };
const n: i32 = total([&sq, &r]); // 10
Spelling. The three forms borrow the value, borrow it mutably, or point at it. A trait object is a type of its own, not a reference or a pointer: it is two words, the value's address and its table's, 16 bytes at 8-byte alignment in every form, and it is copied as a reference is, owning nothing. The suffixes after an object apply to it: virtual Shape*? is an optional object pointer, [virtual &Shape] an array of objects, and virtual Shape** a pointer to an object pointer.
virtualwritten on anything but a trait - a struct, a primitive, a type parameter - is refused (E0316), and so is a trait object anywhere in anexternfunction's signature, whose C side has no table to call through (E0316).- A trait object has no fields:
s.widthis refused (E0204), whatever the value behind it holds. deleteof a trait object is refused (E0200): the size and destructor of the value behind it are not known where the object is used.
Type arguments. The trait's arguments are written after its name, virtual &Conv<i64>: its type parameters first, then its associated types, each in the order the trait declares it, so virtual mut &Iterator<i32> binds Iterator's Item. An object writes one argument for each of them, no more and no fewer (E0307: "Iterator takes 1 type argument in an object type, and 0 are written"). A generic function's parameter is inferred through an object's arguments as through an instantiation's.
Conversion. A value becomes an object wherever it is checked against a declared object type - an initializer, an assignment, an argument, a return, a struct-literal field, an array element: &c becomes a virtual &Shape or a virtual mut &Shape, as it becomes a &Circle or a mut &Circle parameter; a pointer c_ptr: Circle* becomes a virtual Shape*. The object holds the value's address and the table of its type's implementation of the trait. An object converts to no other object, a reference to no virtual Shape*, and a pointer to no borrowed object (E0200).
- The value's type must implement the trait, at the object's arguments: its implementation's trait arguments and associated types must be the ones the object writes (
E0315: "Squaredoes not implementShape, so&Squarecannot becomevirtual &Shape"; "Squaredoes not implementConv<i32>, ..." for aSquareimplementing onlyConv<i64>). - A value whose implementation is generic does not convert yet, nor does a value of a type parameter, whose implementation is not known in the generic body (
E0315).
Supertraits. The table holds the methods of every supertrait of the object's trait as well, so the value's type must implement each of them, directly or through another supertrait (E0315: "Square does not implement Named, which Shape requires, so &Square cannot become virtual &Shape"). A supertrait's arguments are the ones the subtrait writes on it, with the object's arguments put in for the subtrait's own: an object of Sized<i64>, with type trait Sized<U> : Measure<U>, needs the value's type to implement Measure<i64>. An associated type of a supertrait written without arguments, type trait Twice : Pick, is bound by no object of the subtrait, and the value's type may bind it to anything.
Calls. s.area() on an object calls the method its trait or one of its supertraits declares under that name, through the table: the function of the value's type's implementation of the declaring trait at the method's position, or that trait's default where the implementation writes none, with the value's address as its receiver.
- Which trait answers is the nearest, as for a bound: the object's own trait, then its supertraits in the order they are written, then theirs. An object of
LabelcallsLabel'sareaeven when the value's type also implements an unrelatedShapedeclaring one, and a subtrait's ownidis called before its supertrait's. - A method none of them declares is refused (
E0358), and so is a type argument written on the call,s.area::<i32>(): a method callable through an object has no type parameters of its own (E0307, "Shape::areahas 0 type parameters, and 1 type argument is written"), as on a call of any method that has no type parameters of its own (s.double::<i32>()). - A call through a reference or a pointer to an object,
r.area()withr: &virtual &Shapeorp: virtual Shape**, at any depth, calls through the object it reaches, as a call on the object itself does. - A call sees the method's parameters and return with the object's arguments put in:
nextthrough avirtual mut &Iterator<i32>returnsOption<i32>, andconvertthrough avirtual &Conv<i64>returnsi64.
Object safety. A method is callable through an object only when its signature names the implementing type as its receiver alone: a &this or mut &this receiver, no type parameters of its own, and This in no other parameter and not in its return, except as the base of an associated type, This::Item, which the object's arguments bind. Any other method is refused at a call through an object (E0314), with the reason in a note, as is one whose signature names an associated type the object does not bind (a supertrait's, written without arguments); the object's other methods stay callable, and the table holds no function for it. A mut &this method is callable through either borrowed form, as a mut &T parameter accepts either reference.
An object satisfies its own trait. An object satisfies a bound of its own trait and of each of its supertraits, in every form: Keeper<virtual mut &Counter> is accepted where type struct Keeper<C> where C: Counter, and so is ident(o) with o: virtual &Counter where function ident<B>(b: B) -> i32 where B: Base and Base is a supertrait of Counter. No implementation is written for it; the compiler makes one, in which each method is called through the object's table, so a bounded call in the generic body reaches the implementation of the value behind the object, a default the table holds included, and one instance of Keeper<virtual mut &Counter> serves every type implementing Counter. An associated type of the trait, S::Item with S the object virtual &Source<i64>, is the object's own argument, i64.
- Every method of the bound's trait and its supertraits that has no default must be callable through an object: a trait with such a method that has type parameters of its own, or names
Thisoutside its receiver, is satisfied by no object (E0306, "the trait boundvirtual &Grow: Growis not satisfied"). The note names the first such method and why a table cannot hold it, in the words a call of it through the object is refused with (E0314): "the methodgrowofGrowhas no default, and cannot be called throughvirtual &Grow: it has type parameters of its own, and a table holds one copy of each method". No implementation can be written for an object, so the help does not ask for one: "use a type that implementsGrowin place ofvirtual &Grow, or an object of a trait whose methods a table can hold". An object whose trait neither is nor inherits the bound's trait never satisfies it, and its help says so: "use a type that implementsSizein place ofvirtual &Grow; an object implements only its own trait and that trait's supertraits". - A default the table cannot hold -
map<B>andfilterofIterator<i32>, ortake, which returnsThis- does not stand in the way: a bounded call of it is compiled once per object type, in the trait's module, withThisthe object, and each call on the receiver inside it goes through the table. Soit.map(f)infunction sum<I>(it: I) where I: Iterator<i32>runs withIthe objectvirtual mut &Iterator<i32>, callingnextof the value behind it. - The value behind an object a generic struct holds is borrowed, not owned, and nothing checks that it outlives the struct.
Overrides of such a default. The instance compiled for an object is the trait's default and nothing else: a type that writes its own map in its implementation of Iterator has it called directly and under a bound, but not through an object, which reaches the default. A default the table holds is not affected: its slot holds the type's own override where the implementation writes one.
null and comparison. null is a virtual Shape* where one is expected, both words null. A virtual Shape* compares with the null literal by == and !=, testing the value's address; it compares with nothing else, and a borrowed object does not compare at all (E0229).
The table's layout and the object's calling convention are in docs/abi.md; the symbols of a table and of an object type in docs/cryo-mangling-spec.md.
type 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 trait Name : Base {
method(&this, other: &This) -> Ret;
}Names a set of methods a type may implement. Inside the body This is the implementing type; methods may carry default bodies, and a trait may require a base trait (type trait Ord : Eq). Generic code asks for a trait with where T: Name.
type trait EqEq::equals(&this, other: &This) -> boolean&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.
(parameter) other: &ThisThisThe implementing type, inside a trait or implement body. other: &This in a trait method is a reference to whichever type implements it; This::Item names an associated type.
type trait ReadRead::read(mut &this, buf: u8*, len: u64) -> Result<u64, IoError>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.
(parameter) buf: u8*(parameter) len: u64type struct IoError {
kind: IoErrorKind;
os_code: i32;
}Read::read_all(mut &this, out: mut &Array<u8>) -> Result<u64, IoError>Default: keep reading until end-of-stream or error.
(parameter) out: mut &Array<u8>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::arraytype trait Ord : Eqtype trait EqOrd::compare(&this, other: &This) -> Orderingtype enum Ordering {
Less;
Equal;
Greater;
}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 area_of(p: Shape) -> i32(parameter) p: Shapefunction 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 area_of(p: &Shape) -> i32(parameter) p: &Shapefunction area_of<T>(p: &T) -> i32
where T: Shapetype parameter Twhere T: Shape
(parameter) p: &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.
function area_of(p: virtual &Shape) -> i32(parameter) p: virtual &Shapevirtual &Trait
virtual mut &Trait
virtual Trait*Makes a trait object: a value of any type implementing the trait, held with a table of that type's methods for it, so code taking one is compiled once for every implementing type. The three forms borrow the value, borrow it mutably, or point at it. Written on anything but a trait it is refused (E0316); written on a method, it is E0117.
language referenceimplement 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.
implement trait Trait for Type { ... }Introduces a trait implementation: the block supplies, for one concrete type, the method bodies the trait requires.
language referenceimplement trait Trait for Type { ... }Names the type a trait implementation is for. The block that follows supplies the bodies the trait requires.
language referencei32::equals(&this, other: &i32) -> boolean(parameter) other: &i32return 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) other: &i32type trait Ord : Eqi32::compare(&this, other: &i32) -> Orderingif (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) other: &i32Ordering::LessOrdering::GreaterOrdering::Equaltype trait ShapeShape::area(&this) -> i32Shape::scale(mut &this, k: i32) -> void(parameter) k: i32type trait Shapetype 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 SqSq::area(&this) -> i64Sq::scale(&this, k: i32) -> voidfunction smallest<T>(xs: &Array<T>) -> Option<T>
where T: Ord + Copytype parameter Twhere T: Ord + Copy
(parameter) xs: &Array<T>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::marker(parameter) xs: &Array<T>Array::length(&this) -> i64Option::Nonemut 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 best: TArray::get(&this, index: u64) -> Option<T>
where T: CopyBounds-checked read by value. Gated on T: Copy: the result is an independent bitwise duplicate while the array retains ownership of the stored element, so handing back a copy is only sound when T carries no Drop obligation.
Option::unwrap(&this) -> TReturn the inner value, aborting on None. Prefer unwrap_or or a match in code that must not panic.
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.
mut i: u64mut i: u64value 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 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 next: Tconst next: TOrd::compare(&this, other: &This) -> Orderingmut best: TOption::Some(T)type struct Holder<T> where T: Taggedtype parameter Twhere T: Tagged
Holder.inner: THolder::label(&this) -> i32Holder.inner: Ttype enum Name {
Unit,
Payload(T),
Named { x: i32 },
}An algebraic data type. Variants may be bare or carry a tuple or struct payload, and a match on an enum must cover every variant. Name::Variant names a variant; methods are added with implement enum Name { ... }.
type enum Slot<T> where T: TaggedSlot::EmptySlot::Full(T)type trait Sub : MissingSub::go(&this) -> i32function f<T>(x: &T) -> i32
where T: Nowheretype parameter Twhere T: Nowhere
(parameter) x: &Ttype struct Sink<W>type parameter WSink.w: Wtype parameter WSink::total(&this) -> u64Sink.w: Wfunction add<T>(a: T, b: T) -> Ttype parameter T(parameter) a: Ttype parameter T(parameter) b: T(parameter) a: T(parameter) b: Tfunction less<T>(a: T, b: T) -> boolean
where T: Ordtype parameter Twhere T: Ord
(parameter) a: T(parameter) b: Ttype struct Cell<T, A>type parameter ACell.v: Ttype parameter TCell.tag: Atype parameter Aimplement struct Name { ... }Names the struct an inherent block extends. The methods inside are interchangeable with ones declared in the struct body.
language referencetype struct Cell<T, A>Cell::tagged(&this) -> i32Cell.tag: ACell::tag_ref(&this) -> &Atype parameter Afunction first<T>(xs: &Array<T>) -> T
where T: Copytype parameter Twhere T: Copy
const v: T(parameter) xs: &Array<T>const v: Tfunction push_first<T>(xs: mut &Array<T>) -> void
where T: Copy(parameter) xs: mut &Array<T>(parameter) xs: mut &Array<T>Array::push(mut &this, value: T) -> voidfunction first_of<T>(xs: &Array<T>) -> Option<T>
where T: Clonetype parameter Twhere T: Clone
(parameter) xs: &Array<T>function halve<T>(x: T) -> T(parameter) x: Ttype parameter Tstatic 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.
f64pattern => { body }Separates a match arm's pattern from its body. Arms do not fall through.
const h: f64(parameter) x: Tconst h: f64_type trait Iteratortype Item;
type Output = i64;An associated type on a trait: named in the trait body, filled in by each implementation, and referred to as This::Item or I::Item.
type ItemIterator::next(mut &this) -> Option<This::Item>type Itemtype struct MapIter<I, O> where I: Iteratortype parameter Iwhere I: Iterator
type parameter Otype trait IteratorMapIter.inner: IMapIter.f: (I::Item) -> OIterator::type ItemThe element type this iterator yields. Each implementor binds it once (positional sugar: implement Iterator<T> for X => Item = T); refer to it as This::Item inside the trait and X::Item outside.
type parameter Otype trait DoubleEnded : IteratorDoubleEnded::next_back(mut &this) -> Option<This::Item>function first<I>(it: mut &I) -> Option<I::Item>
where I: DoubleEndedtype parameter Iwhere I: DoubleEnded
(parameter) it: mut &Itype trait DoubleEnded : Iterator(parameter) it: mut &Itype struct Loose<I>type parameter ILoose.f: () -> I::Itemtype parameter Itype struct Countertype struct Countertype Item = i32type trait SeqSeq::next_one(&this) -> i32type trait Seqtype struct Holder<T> where T: Taggedtype struct Vec2Vec2.x: i64Vec2.y: i64type trait Add<Rhs, Output> {
add(&this, rhs: &Rhs) -> Output;
}Arithmetic operator traits. Implementing one lets the compiler rewrite the corresponding operator on a user type (or a generic param bounded by the trait) into the trait method: a + b => a.add(&b), and likewise - => sub, * => mul, / => div, % => rem.
type struct Vec2Vec2::add(&this, rhs: &Vec2) -> Vec2(parameter) rhs: &Vec2Vec2.x: i64(parameter) rhs: &Vec2Vec2.y: i64type trait Mul<Rhs, Output> {
mul(&this, rhs: &Rhs) -> Output;
}Vec2::mul(&this, rhs: &i64) -> Vec2(parameter) rhs: &i64(parameter) rhs: &i64type trait Neg<Output> {
neg(&this) -> Output;
}Unary negation: -x => x.neg(). Same type-directed discipline as the binary arithmetic traits; numeric negation stays on the native path.
Vec2::neg(&this) -> Vec2const 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 a: Vec2const b: Vec2const c: Vec2const a: Vec2const b: Vec2const d: Vec2const e: Vec2type struct Vec2Vec2::equals(&this, other: &Vec2) -> boolean(parameter) other: &Vec2Vec2.x: i64(parameter) other: &Vec2Vec2.y: i64Vec2::compare(&this, other: &Vec2) -> Ordering(parameter) other: &Vec2const eq: booleanconst a: Vec2const b: Vec2const lt: booleantype struct GridGrid.cells: [i32; 9]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).
type struct GridGrid::index(&this, i: u64) -> i32*(parameter) i: u64Grid.cells: [i32; 9](parameter) i: u64mut g: Gridmut g: Gridtype struct Boxed<T>Boxed.value: Ttype parameter Ttype trait Deref<Target> {
deref(&this) -> Target*;
}Deref<Target> exposes the value a smart pointer points at. The returned Target* is mutable (Cryo pointers are not const-qualified), so this one trait covers both shared and mutable access - there is no separate DerefMut.
type parameter Ttype struct Boxed<T>Boxed::deref(&this) -> T*Boxed.value: Tmut b: Boxed<Vec2>const v: Vec2mut b: Boxed<Vec2>function sum<T>(a: T, b: T) -> T
where T: Add<T, T>type parameter Twhere T: Add
(parameter) a: T(parameter) b: Tfunction sum<T>(a: T, b: T) -> T
where T: Add<T, T>type struct SquareSquare.side: i32type struct RectRect.w: i32Rect.h: i32type trait Shapetype struct SquareSquare::area(&this) -> i32Square.side: i32type struct RectRect::area(&this) -> i32Rect.w: i32Rect.h: i32function total(shapes: [virtual &Shape]) -> i32(parameter) shapes: [virtual &Shape]mut sum: i32s: virtual &Shapefor (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 } }.
(parameter) shapes: [virtual &Shape]mut sum: i32s: virtual &ShapeShape::area(&this) -> i32const sq: Squareconst r: Rectconst n: i32function total(shapes: [virtual &Shape]) -> i32const sq: Squareconst r: Recttype struct Squaretype trait Sized<U> : Measure<U>type parameter Utype struct Box<T, A = GlobalAlloc> where A: Allocator {
ptr: T*;
alloc: A;
}Box<T, A>: single-owner heap allocation.
std::alloc::boxtype trait Shapetype struct Pair<A, B> {
first: A;
second: B;
}Pair<A, B>: an owned two-element tuple.
std::collections::pairtype trait From<T> {
static from(value: T) -> This;
}TestFormat::Plaintype trait Default {
static default() -> This;
}Default: a canonical "zero value" for a type.
std::core::defaultstatic name(param: Type) -> Ret { ... }A method that belongs to the type rather than an instance, called as Type::name(..). For structs, static new(..) returning a struct literal is the idiomatic constructor.
type trait Hasher {
write_u8(mut &this, value: u8) -> void;
write_bytes(mut &this, bytes: Slice<u8>) -> void;
write_uint(mut &this, value: u128, byte_count: u32) -> void;
fold<T>(mut &this, value: T) -> void;
finish(&this) -> u64;
}type trait Sub : Missingtype trait Div<Rhs, Output> {
div(&this, rhs: &Rhs) -> Output;
}type trait Rem<Rhs, Output> {
rem(&this, rhs: &Rhs) -> Output;
}type trait BitAnd<Rhs, Output> {
bitand(&this, rhs: &Rhs) -> Output;
}Bitwise and shift operator traits, rewritten exactly like the arithmetic ones: a & b => a.bitand(&b), and likewise | => bitor, ^ => bitxor, << => shl, >> => shr.
type trait BitOr<Rhs, Output> {
bitor(&this, rhs: &Rhs) -> Output;
}type trait BitXor<Rhs, Output> {
bitxor(&this, rhs: &Rhs) -> Output;
}type trait Shl<Rhs, Output> {
shl(&this, rhs: &Rhs) -> Output;
}type trait Shr<Rhs, Output> {
shr(&this, rhs: &Rhs) -> Output;
}type trait Not<Output> {
not(&this) -> Output;
}Unary complement traits. !x => x.not() (logical, for a user "boolean-like" type) and ~x => x.bitnot() (bitwise). Same type-directed discipline as Neg; the primitive bool/integer forms stay on the native path.
type trait BitNot<Output> {
bitnot(&this) -> Output;
}type trait Into<T> {
into(&this) -> T;
}type trait TryFrom<T> {
static try_from(value: T) -> Result<This, ConversionError>;
}type trait TryInto<T> {
try_into(&this) -> Result<T, ConversionError>;
}type trait Readtype trait Write {
write_some(mut &this, bytes: Slice<u8>) -> Result<u64, IoError>;
flush(mut &this) -> Result<(), IoError>;
write<T>(mut &this, data: T) -> Result<(), IoError>;
}type trait Display {
fmt<W>(&this, f: mut &Formatter<W>) -> Result<(), FmtError>
where W: FmtWrite;
}Display / Debug / Formatter.
std::fmt::displaytype trait Debug {
fmt<W>(&this, f: mut &Formatter<W>) -> Result<(), FmtError>
where W: FmtWrite;
}type trait FmtWrite {
write_str(mut &this, source: Str) -> Result<(), FmtError>;
write_char(mut &this, c: char) -> Result<(), FmtError>;
}type trait Allocator {
allocate(mut &this, layout: Layout) -> Result<NonNull<u8>, AllocError>;
deallocate(mut &this, ptr: NonNull<u8>, layout: Layout) -> void;
reallocate(mut &this, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout) -> Result<NonNull<u8>, AllocError>;
}Allocator: the trait every heap strategy implements.
std::alloc::allocatorArray::iter(&this) -> implement Iterator<T>
where T: CopyIterate over the elements in order, yielding each by value (bitwise copy). Gated on T: Copy for the same reason as get: a yielded owning value would alias storage the array still owns. For owning T, walk by index with length() + get_ref.
type struct HashMap<K, V, A = GlobalAlloc> where A: Allocator {
buckets: Entry<K, V>**;
bucket_count: u64;
length: u64;
alloc: A;
seed: u64;
}HashMap<K, V, A>: key-value map with amortized O(1) operations.
std::collections::hashmapHashMap::keys(&this) -> implement Iterator<K>
where K: CopyIterate over the keys, yielding each by value (bitwise copy). Gated on K: Copy for the same reason as iter. Order is bucket-major and unstable across resizes.
type struct Str {
bytes: u8*;
length: u64;
}Str: a borrowed UTF-8 string slice.
std::collections::strStr::split(&this, sep: &Str) -> implement Iterator<Str>Iterator over the (non-overlapping) pieces between occurrences of the byte separator sep. A leading/trailing/doubled separator yields an empty piece. Each yielded Str borrows from this one. split on a one-byte separator is the common case; multi-byte separators are supported too.
type struct TakeIter<I> {
inner: I;
remaining: u64;
}Adapter yielding at most remaining elements from inner.
type 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 Item: Copytype Item = ...;type Out = ...;type struct MapIter<I, O> where I: Iteratortype struct Range<T> {
start: T;
end: T;
}Vec2::add(&this, rhs: &Vec2) -> Vec2Vec2::neg(&this) -> Vec2Vec2::equals(&this, other: &Vec2) -> booleanVec2::compare(&this, other: &Vec2) -> OrderingOrdering::is_lt(&this) -> booleanOrdering predicates backing the relational operators on Ord types. The compiler rewrites a < b into a.compare(&b).is_lt() (and >, <=, >= into is_gt/is_le/is_ge), so these keep the operator semantics in one place and let Ord-only types get < > <= >= for free once they implement compare.
type struct Output {
status: ExitStatus;
stdout: Array<u8>;
stderr: Array<u8>;
}Index::index(&this, idx: Idx) -> Output*const v: Vec2Deref::deref(&this) -> Target*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.
Add::add(&this, rhs: &Rhs) -> Outputextern 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 referencedelete p;Releases what new T { .. } allocated: the pointee's Drop and the values its fields own, then its storage, through T's Allocate::release. Memory from new T[n] or malloc is released with free instead.
const c: Vec2type trait Twice : PickShape::area(&this) -> i32const r: RectRect::area(&this) -> i32type struct Countertype struct Keeper<C> where C: Countertype parameter Cwhere C: Counter
function ident<B>(b: B) -> i32
where B: Basetype parameter Bwhere B: Base
(parameter) b: Bfunction f<T>(x: &T) -> i32
where T: Nowherefunction sum<I>(it: I)
where I: Iterator<i32>(parameter) it: I