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], (), ?, .awaitleft—
2-x, !x, ~x, &x, &mut x, &checked x, &unsafe x, *xprefixNeg, Not, BitNot / Deref
3as (cast)leftFrom, Into
4** (exponent)rightPow
5*, /, %leftMul, Div, Rem
6+, -leftAdd, Sub
7<<, >>leftShl, Shr
8& (bitand)leftBitAnd
9^ (bitxor)leftBitXor
10| (bitor)leftBitOr
11==, !=, <, <=, >, >=, in, isleftEq, Ord / PartialOrd
12&& (logical and)leftshort-circuit
13|| (logical or)leftshort-circuit
14??, .., ..=, -> / implies, =>, <-> / iffright—
15=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=rightAddAssign, SubAssign, …
16|> (pipe)left—

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

The three bitwise operators are three levels, not one — & tighter than ^ tighter than |, as in C. So a | b & c is a | (b & c), and diff | a ^ b accumulates a ^ b into diff rather than xor-ing diff | a.

Comparison is a single left-associative level, so a == b < c groups as (a == b) < c. Chained comparisons parse and are then rejected by the type checker (a Bool compared with a number); write a < b && b < c instead. is and in sit on this level too.

Assignment takes the whole right-hand side: x = a ?? b is x = (a ?? b). Only the pipeline |> is looser, so x = a |> f groups as (x = a) |> f — parenthesise if you meant to pipe the value: x = (a |> f).

A prefix operator binds tighter than as: -a as Int is (-a) as Int, and -2 ** 2 is (-2) ** 2.

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 *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 / record-update (in patterns and record exprs).
@Pattern alias binding; x @ Pattern binds x and tests.
->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 1.2–1.7 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 = [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, binds x
while result is Pending() { poll(); } // loop condition
if x is not None { use(x); } // negation

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

note
is takes no guard, and the type test is a pattern

A guard after the pattern is a parse error. There is no is P if cond; matches!(x, P if cond) becomes a match with a guarded arm.

if v is Some2(n) if n > 0 { ... }
error<E018>: Parse error: unexpected keyword `if`, expected `{`

The type test is a PATTERN, not an expression. let flag = value is Int; is refused — "Pattern 'Int' is not a defined constant" — because the grammar puts x is Type under patterns. In pattern position it works:

let v: Int = 7;
match v { x is Int => "Int", _ => "other" } // "Int"

The type test compares the value's runtime type with the declared type. It does not compare a type-name string index with a variant tag.

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: *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 ?.​

Equivalent to:

user?.address?.city?.namereturns Maybe.None if any step is None
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​