Skip to content
CryoCryo home
LanguageFoundations

5Operators

5.1 Arithmetic

OperatorDescription
+ - * / %Add, subtract, multiply, divide, modulo
- (unary)Negation
++ --Pre/postfix increment, decrement

Integer division truncates toward zero. The prefix forms ++x, --x evaluate to the new value; the postfix forms x++, x-- evaluate to the old value (C semantics).

Overflow. Integer arithmetic wraps on overflow using two's-complement modular arithmetic, for both signed and unsigned types: the result is reduced modulo 2N for an N-bit type. There is no overflow trap and no automatic widening. For example, with i32:

mut x: i32 = 2147483647;   // i32::MAX
x = x + 1;                 // wraps to -2147483648 (i32::MIN), no trap

This is a defined deterministic result, not undefined behavior, but it is silent: the language does not insert checks. Code that must detect overflow has to compare against the type's bounds before the operation. (Unsigned wrap is the usual mod 2N; e.g. 0u8 - 1u8 == 255.)

Division and modulo by zero are not checked by the compiler and fault at runtime (on typical targets the CPU raises SIGFPE); the signed i32::MIN / -1 / i32::MIN % -1 cases overflow the result and fault the same way. Guard the divisor when it can be zero.

5.2 Comparison

OperatorDescription
== !=Equal / not equal
< > <= >=Ordering comparisons
<=>Three-way comparison (spaceship); yields an Ordering (Less / Equal / Greater)

Comparison operators return boolean. On numeric types and pointers they emit native instructions; on user-defined types that implement Eq/Ord they are overloaded - a == b becomes a.equals(&b) and a < b becomes a.compare(&b).is_lt() (see operator overloading, section 10.6).

Unrelated types are not compared. Two values of different enum types, or two pointers neither of which converts to the other, are refused (E0200) by every comparison operator. An enum compares its discriminant, so A::Z == B::W would otherwise answer by variant position; two pointers to unrelated structs never hold the same address. A pointer still compares with null, with a void*, and with a pointer it converts to (T* with const T*):

type enum A { X; Z; }
type enum B { Y; W; }

const same: boolean = A::Z == B::W;   // error[E0200]: cannot compare `A` with `B`

A reference is not compared with a plain value. When one operand is a reference and the other is a number, boolean, char, string or enum, the comparison is refused (E0229) - the same rule arithmetic follows, where x % 2 with x: &int is already an error. Dereference explicitly:

function is_seven(x: &i32) -> boolean {
    return x == 7;     // error[E0229]: a reference compared with a value
    return *x == 7;    // compares the value x refers to
}

The alternative - quietly dereferencing - was rejected because the comparison did not previously dereference: it compared the reference's address with 7, which compiled and was silently false. Refusing it makes the one meaning explicit at the call site. What stays allowed: two references compared with each other (by value when the referent implements Eq/Ord, by identity otherwise), and a reference compared with null.

5.3 Logical

OperatorDescription
&&Logical AND (short-circuiting)
||Logical OR (short-circuiting)
!Logical NOT (unary)

5.4 Bitwise

OperatorDescription
& | ^AND / OR / XOR
~Bitwise NOT (unary)
<< >>Left / right shift (arithmetic on signed, logical on unsigned)

5.5 Assignment

Only mut bindings can be assigned to.

OperatorDescription
=Simple assignment
+= -= *= /= %=Compound arithmetic
&= |= ^= <<= >>=Compound bitwise

5.6 Other Operators

OperatorDescription
->Return-type arrow. Not a member-access operator: . auto-dereferences pointers, so write p.field, never p->field.
=>Pattern-to-body separator inside match.
::Scope resolution: static methods, enum variants, module members.
? :Ternary conditional.
? (postfix)Error propagation ("try"): on a Result/Option, yields the Ok/Some payload, else returns the Err/None from the enclosing function.
??Null-coalescing: opt ?? fallback yields a Some's payload, else fallback (evaluated only when opt is None).
|> <|Pipeline: thread a value into a call (x |> f(a) => f(x, a); f(a) <| x => f(a, x)).
asExplicit type cast.
.Member access.
&Address-of (unary).
*Dereference (unary).
sizeof(T)Compile-time size of T in bytes.
alignof(T)Compile-time alignment of T in bytes.
typeof(expr)Compile-time type of expr, used in type position.
new deleteHeap allocation / deallocation.

Reserved. ?. and ... in call position are recognised by the lexer but not yet lowered. See section 21. (The range operators .. / ..= are fully lowered - see section 5.7.)

Pipeline (|>, <|). The pipeline operators thread a value into a call. x |> f is f(x); with an argument list the piped value is prepended - x |> f(a, b) is f(x, a, b). The backward form appends instead - f(a, b) <| x is f(a, b, x). Pipes are left-associative, so x |> f |> g is g(f(x)). They are a compile-time rewrite to an ordinary call, with no runtime cost.

const out: int = data |> parse |> validate(strict);   // validate(parse(data), strict)

Null-coalescing (??). opt ?? fallback unwraps an Option<T> to its T, substituting fallback when it is None. The left operand is evaluated once and fallback only when needed. ?? is right-associative and binds looser than the pipes, so a chain reads as a ?? (b ?? c): every operand but the last is an Option<T>, and the final c is the bare T.

const port: u16 = config_port() ?? env_port() ?? 8080;

Error propagation (?). A postfix ? on a Result<T, E> evaluates to T when the value is Ok, and otherwise returns that Err(e) unchanged from the enclosing function; on an Option<T> it yields T for Some and returns None. The enclosing function's return type must be a matching Result / Option. It is the concise form of a match that re-returns the error.

function load(path: string) -> Result<Config, IoError> {
    const text: String = read_file(path)?;     // returns Err(e) on failure
    return Result::Ok(parse_config(text));
}

Type-of (typeof). typeof(expr) resolves to the static type of expr and is used in type position - anywhere a type annotation is expected: variable bindings, pointer/array/optional wrappers, generic arguments, and as cast targets. It is a compile-time construct that names a type, not a value, so it cannot appear where a value is expected. expr is only type-checked, never evaluated.

const x: i32 = read_count();
const y: typeof(x) = 0;        // y : i32
const p: typeof(x)* = &x;      // p : i32*
const n: i64 = 5;
const back = n as typeof(x);   // back : i32

5.7 Operator Precedence

From lowest to highest:

LevelOperatorsAssociativity
1= += -= *= /= %= &= |= ^= <<= >>=Right
2?? (null-coalescing)Right
3|> <| (pipeline)Left
4? : (ternary)Right
5.. ..= (range)Left
6||Left
7&&Left
8|Left
9^Left
10&Left
11== !=Left
12< > <= >= <=>Left
13<< >>Left
14+ -Left
15* / %Left
16asLeft
17- ! & * ~ ++ -- (unary prefix), new, deleteRight
18() [] . ? (postfix try) ++ -- (postfix)Left

as sits between multiplication and unary, so x * y as i64 casts y, not the product. Use parentheses if you mean (x * y) as i64.

The range operators .. / ..= bind looser than every arithmetic and comparison operator, so a + 1 .. b * 2 is (a + 1) .. (b * 2). They desugar at parse time to Range::new / RangeInclusive::new (see section 6.3).