Skip to main content

Operators

Every operator in Verum, by category, with precedence, associativity, and the protocol you overload to change its behaviour.

Precedence table

From tightest (evaluated first) to loosest.

PrecOperatorsAssociativityOverload protocol
1.field, .method(), ?., [idx], () — method/index/callleft
2.await, ? (postfix)left
3as (cast)leftFrom, Into
4is (pattern test)left
5-x, !x, ~x (prefix unary)prefixNeg, Not, BitNot
6&x, &mut x, &checked x, &unsafe x, *x (ref/deref)prefix— / Deref
7** (exponent)rightPow
8*, /, %leftMul, Div, Rem
9+, -leftAdd, Sub
10<<, >>leftShl, Shr
11& (bitand)leftBitAnd
12^ (bitxor)leftBitXor
13| (bitor)leftBitOr
14.., ..= (range)none
15==, !=, <, <=, >, >=noneEq, Ord / PartialOrd
16&& (logical and)leftshort-circuit
17|| (logical or)leftshort-circuit
18?? (null coalesce)left
19|> (pipe)left
20=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=rightAddAssign, SubAssign, …

Lower number = tighter binding. a + b * c parses as a + (b * c) because * (8) binds tighter than + (9).

Non-associative operators (range, comparison) do not chain: a < b < c is a parse error; use a < b && b < c.

Overloadable operators

Implement the listed protocol to make an operator work on your type.

OperatorProtocolMethod signature
a + bAdd<Rhs = Self>fn add(self, rhs: Rhs) -> Self.Output
a - bSubfn sub(self, rhs: Rhs) -> Self.Output
a * bMulfn mul(self, rhs: Rhs) -> Self.Output
a / bDivfn div(self, rhs: Rhs) -> Self.Output
a % bRemfn rem(self, rhs: Rhs) -> Self.Output
a ** bPowfn pow(self, rhs: Rhs) -> Self.Output
-aNegfn neg(self) -> Self.Output
!aNotfn not(self) -> Self.Output
~aBitNotfn not(self) -> Self.Output
a & bBitAndfn bitand(self, rhs: Rhs) -> Self.Output
a | bBitOrfn bitor(self, rhs: Rhs) -> Self.Output
a ^ bBitXorfn bitxor(self, rhs: Rhs) -> Self.Output
a << bShlfn shl(self, rhs: Rhs) -> Self.Output
a >> bShrfn shr(self, rhs: Rhs) -> Self.Output
a += bAddAssignfn add_assign(&mut self, rhs: Rhs)
a -= bSubAssignfn sub_assign(&mut self, rhs: Rhs)
(similarly for *=, /=, %=, &=, |=, ^=, <<=, >>=)
a == bEq, PartialEqfn eq(&self, other: &Self) -> Bool
a < bOrd, PartialOrdvia cmp(&Self) -> Ordering
a[i]Index<Idx>fn index(&self, i: Idx) -> &Self.Output
a[i] = vIndexMut<Idx>fn index_mut(&mut self, i: Idx) -> &mut Self.Output
*aDeref<Target = T>fn deref(&self) -> &Self.Target
a()FnOnce / FnMut / Fn(closure protocols — see below)

Closure protocols

Function types come in three flavours:

  • FnOnce — consumes captured values; callable once.
  • FnMut — may mutate captures; callable many times.
  • Fn — immutable captures only; callable many times and re-entrantly.

FnMut: FnOnce and Fn: FnMut: FnOnce — a more capable closure type subsumes a less capable one.

Non-overloadable operators

These operators have fixed semantics:

OperatorMeaning
&&Logical AND, short-circuit (right side skipped if left is false).
||Logical OR, short-circuit.
=Assignment.
..Exclusive range.
..=Inclusive range.
?Propagate Result.Err / Maybe.None.
.awaitFuture suspension.
isPattern test (value is Pattern, x is Type).
asType cast (x as Int, &x as *unsafe mut T).
|>Pipe — a |> f(args) desugars to f(a, args).
??Null-coalesce — a ?? b = a.unwrap_or(b).
.Field / method access.
?.Optional chaining — a?.b = a.and_then(|x| x.b).
..Rest / struct-update (in patterns and record exprs).
@Pattern alias binding; x @ Pattern binds x and tests.
::Turbofish (expression-position type args).
->Function type / return type arrow.
=>Match-arm arrow / closure / lambda.

Range operators

0..10 // exclusive: 0, 1, ..., 9 Range<Int>
0..=10 // inclusive: 0, 1, ..., 10 RangeInclusive<Int>
..10 // prefix: anything up to 10 RangeTo<Int>
0.. // suffix: 0 and above RangeFrom<Int>
.. // unbounded RangeFull

Ranges are values. They implement Iterator when the element type is ordered and incrementable (integers, characters). Float ranges don't iterate — use stride(0.0, 1.0, 0.1) or a manual for.

Unary reference operators

&x // tier-0 managed (CBGR ~15 ns)
&checked x // tier-1 compiler-proven (0 ns)
&unsafe x // tier-2 programmer-proven (0 ns), needs `unsafe` block
&mut x // mutable reference
&checked mut x
&unsafe mut x
*x // dereference — safe for &T/&checked T; unsafe for &unsafe T and raw pointers

See language/references.

Compound assignment

a += b is sugar for a = a + b, but the compiler calls the AddAssign protocol when implemented, avoiding a temporary copy:

let mut xs = vec![1, 2, 3];
xs += 4; // desugars to xs.add_assign(4)

Every binary arithmetic/bitwise operator has a *Assign counterpart.

The is operator

if value is Maybe.Some(x) { ... } // pattern test
while result is Pending() { poll(); } // loop condition
if x is not None { use(x); } // negation
let flag = value is Int; // type-test pattern

is has the same precedence as ==/</> (level 15). It's a pattern test; bindings introduced by the pattern scope outside the test expression where accessible.

See language/patterns and language/active-patterns.

The as operator

as performs a value conversion — not a raw reinterpret:

let n: Int = 3.7 as Int; // truncates to 3 (via Float.to_int)
let s: Int = "42" as Int; // parses — error at compile time if impossible
let p: *unsafe mut Byte = slice.as_ptr();
let b: Byte = (n as u8);

as dispatches to the From/Into protocol when implemented, or to a compiler-built-in conversion for primitives.

Pipe |> and method pipe |> .method()

let sum =
list
|> .filter(|x| x > 0)
|> .map(|x| x * 2)
|> .sum();

// equivalent to:
let sum = list.filter(|x| x > 0).map(|x| x * 2).sum();

For ordinary functions:

value |> transform(extra_arg)
// desugars to:
transform(value, extra_arg)

The method-pipe form (|> .method(args)) is grammar-level sugar for chainable pipelines; see language/syntax.

Optional chaining ?.

user?.address?.city?.name // returns `Maybe.None` if any step is None

// Equivalent to:
user.and_then(|u| u.address)
.and_then(|a| a.city)
.and_then(|c| c.name)

Each ?.method() short-circuits on Maybe.None; the result type is Maybe<Final>.

Null coalesce ??

let name = preferred ?? default ?? "anonymous";
// preferred and default are Maybe<Text>; "anonymous" is the final fallback

a ?? b returns a.unwrap() if a.is_some(), else b. Chains left-associatively.

Error propagation ?

fn load() -> Result<Config, Error> {
let text = read_file(path)?; // propagate IoError → Error
let cfg = parse(&text)?; // propagate ParseError → Error
Result.Ok(cfg)
}

? is postfix. On Result.Ok(v) it evaluates to v; on Result.Err(e) it returns early with Result.Err(From.from(e)), which converts the inner error type via From.

Also works on Maybe:

fn first(xs: &List<Int>) -> Maybe<Int> {
let head = xs.first()?; // None propagates
Maybe.Some(*head * 2)
}

Precedence examples

a + b * c == b * c + a // parses as: ((a + (b * c)) == ((b * c) + a))
let v = x ?? y |> .foo(); // parses as: let v = (x ?? y) |> .foo();
a && b || c // parses as: ((a && b) || c)
match x is Some(v) && v > 0 { ... } // parses as: match (x is Some(v) && v > 0)
-x.y // parses as: -(x.y)

For ambiguous-looking code, parenthesise.

See also