Operators
Operators perform operations on values (operands).
Arithmetic Operators
Addition (+)
val sum: i32 = 10 + 20 // 30val total: f64 = 3.14 + 2.86 // 6.00Types: Works with numeric types (integers and floats), and with string (concatenation)
Requirement: Both operands must be the same type
On two strings, + is concatenation: it allocates a new owned, immutable string
holding the left operand's bytes followed by the right operand's. A &string slice may stand in
for either side. Operands are read, not consumed, so they remain usable afterward.
val greeting: string = "Hello, " + "Neuro!" // "Hello, Neuro!"val a: string = "ab"val b: string = "cd"val joined: string = a + &b // "abcd"; a and b still validThe concatenated buffer is heap-allocated. Deterministic
Droplanded with 1C, so it is freed where the compiler can prove who owns it: a temporary the statement consumes, or a binding whose initializer allocated it. One that escapes that analysis still leaks, notably a heapstringstored into a collection or a struct field, returned from a function, or displaced by reassigning its binding. See the alpha memory warning in the README.
Subtraction (-)
val diff: i32 = 50 - 20 // 30val delta: f64 = 10.5 - 2.3 // 8.2Types: Works with numeric types Requirement: Both operands must be the same type
Multiplication (*)
val product: i32 = 6 * 7 // 42val area: f64 = 3.14 * 2.0 // 6.28Types: Works with numeric types Requirement: Both operands must be the same type
Division (/)
val quotient: i32 = 20 / 4 // 5val ratio: f64 = 10.0 / 3.0 // 3.333...Types: Works with numeric types Requirement: Both operands must be the same type Note: Integer division truncates (5 / 2 = 2)
Modulo (%)
val remainder: i32 = 17 % 5 // 2val mod: i32 = 10 % 3 // 1Types: Works with integer types Requirement: Both operands must be integers
Comparison Operators
All comparison operators return bool.
Equal (==)
val is_equal: bool = 42 == 42 // trueval same: bool = x == yNot Equal (!=)
val is_different: bool = 42 != 10 // trueval not_same: bool = x != yLess Than (<)
val is_less: bool = 5 < 10 // trueval smaller: bool = x < yGreater Than (>)
val is_greater: bool = 10 > 5 // trueval larger: bool = x > yLess Than or Equal (<=)
val is_lte: bool = 5 <= 5 // trueval at_most: bool = x <= maxGreater Than or Equal (>=)
val is_gte: bool = 10 >= 5 // trueval at_least: bool = x >= minTypes: Work with numeric types, booleans, and strings (==/!= only)
Requirement: Both operands must be the same type
Chaining: Comparison operators cannot be chained. a < b < c is a compile error, write a < b && b < c instead.
Logical Operators
Work with boolean values, return boolean.
Logical AND (&&)
val both: bool = true && true // trueval result: bool = flag1 && flag2val valid: bool = x > 0 && x < 100Short-circuit: If left side is false, right side is not evaluated
Logical OR (||)
val either: bool = true || false // trueval result: bool = flag1 || flag2val valid: bool = x < 0 || x > 100Short-circuit: If left side is true, right side is not evaluated
Logical NOT (!)
val inverted: bool = !true // falseval opposite: bool = !flagUnary operator: Takes single boolean operand
Unary Operators
Negation (-)
val neg: i32 = -42 // -42val opposite: i32 = -xval abs_neg: i32 = -abs(x)Types: Works with numeric types Returns: Same type as operand
On an integer, -x is 0 - x and follows the same overflow rule as the subtraction: it
panics in debug builds and wraps in release ones. That makes it an overflow at a signed
type's MIN, and at every nonzero value of an unsigned type — see
integer overflow.
Logical NOT (!)
val not_true: bool = !true // falseval not_flag: bool = !flagTypes: Works with boolean type only Returns: boolean
Bitwise Operators
Work with integer values only (i8 to i64, u8 to u64). Cannot be used with floats or bools.
Bitwise AND (&)
val a: i32 = 0b1100 // 12val b: i32 = 0b1010 // 10val r: i32 = a & b // 0b1000 = 8Returns: same type as operands
Bitwise OR (|)
val a: i32 = 0b1100 // 12val b: i32 = 0b1010 // 10val r: i32 = a | b // 0b1110 = 14Returns: same type as operands
Bitwise XOR (^)
val a: i32 = 0b1100 // 12val b: i32 = 0b1010 // 10val r: i32 = a ^ b // 0b0110 = 6Returns: same type as operands
Left Shift (<<)
val a: i32 = 1val r: i32 = a << 4 // 1 * 2^4 = 16Returns: same type as operands
Note: Right shift is exposed as the .shr(n) method, not an operator (ashr for signed receivers, lshr for unsigned). See types.md.
Bitwise NOT (~)
val a: i32 = 0val r: i32 = ~a // -1 (all bits set, two's complement)Unary: takes a single integer operand Returns: same type as operand
Type Casting Operator (as)
Performs an explicit numeric or boolean type conversion.
val n: i32 = 42val x: f64 = n as f64 // widen integer to floatval y: i64 = n as i64 // widen to larger integer val pi: f64 = 3.14159val trunc: i32 = pi as i32 // truncate toward zero → 3 val flag: bool = trueval one: i32 = flag as i32 // false → 0, true → 1Types: Works with numeric types and booleans. Rules:
- Widening integers zero-extends (unsigned) or sign-extends (signed).
- Floats to integers truncate towards zero.
- Booleans to integers map
false → 0andtrue → 1.
Assignment Operator (=)
Variable Assignment
mut x: i32 = 10x = 20 // Reassignx = x + 5 // Updatex = add(x, 10) // Assign from expressionRequirement: Variable must be declared with mut
Type checking: Right-hand side must match variable type
Cannot Assign to Immutable
val x: i32 = 10// x = 20 // Error: cannot assign to immutable variableCompound Assignment Operators
Shorthand for updating a mutable variable in-place. Each form is equivalent to a plain assignment with the corresponding binary operator on the right-hand side.
| Operator | Equivalent to |
|---|---|
x += n | x = x + n |
x -= n | x = x - n |
x *= n | x = x * n |
x /= n | x = x / n |
x %= n | x = x % n |
mut score: i32 = 100score += 50 // 150score -= 25 // 125score *= 2 // 250score /= 5 // 50score %= 13 // 11mut sum: i32 = 0mut i: i32 = 1while i <= 10 { sum += i i += 1}Requirement: Left-hand side must be a mut variable
Type checking: Same rules as the underlying binary operator apply
Note: Compound assignment on struct fields (point.x += 1.0) is not yet supported
Null/Error Coalescing Operator (??)
?? is the read-site equivalent of unwrap_or(default), it returns the unwrapped value of an Option<T> or Result<T, E> when present, and falls back to the right-hand expression when absent (None) or failed (Err). The expression's type is the unwrapped payload T.
val present = lookup(1) ?? 0 // the Some payloadval absent = lookup(7) ?? 5 // 5, the fallback val ok = divide(24, 4) ?? 0 // the Ok payloadval failed = divide(1, 0) ?? 4 // 4, the Err payload is discardedFor a Result, the error payload is discarded: ?? states "I do not care why it failed, use this instead". When the reason matters, match on the value instead.
Laziness: the fallback is only evaluated when the left-hand side is absent or failed. A fallback that calls a function, panics, or does real work is skipped entirely when the value is present.
Fallback type: the right-hand side must produce the payload type T, not another Option/Result. A mismatch is an ordinary type error.
Associativity: right-to-left. a ?? b ?? c parses as a ?? (b ?? c), so each fallback is evaluated only when every left-hand side up to it has produced the absent / error variant. Left-to-right would force the middle fallback even when the chain succeeds early, defeating the short-circuit contract. In a chain, every operand but the last must itself be fallible:
val chained = lookup(7) ?? lookup(1) ?? 99Precedence: level 14, looser than || (so a ?? b || c means a ?? (b || c)), tighter than range operators.
Applying ?? to anything that is not an Option<T> or Result<T, E> is rejected:
error: `??` expects an `Option<T>` or `Result<T, E>` on the left, found i32
Payloads are scalar Copy values in this phase (see types.md), so ?? unwraps to a scalar. Runnable program: examples/operators/null_coalesce.nr.
Error Propagation Operator (?)
? is the postfix complement of ??: instead of supplying a fallback, it hands the failure to the caller. expr? evaluates to the unwrapped payload when expr is Some / Ok, and otherwise leaves the enclosing function immediately, carrying the failure variant on.
func quarter(n: i32) -> Result<i32, i32> { val half = halve(n)? // Err(n) leaves quarter() right here val rest = halve(half)? Result::Ok(rest)}It desugars to exactly this match:
val half = match halve(n) { Result::Ok(v) => v, Result::Err(e) => return Result::Err(e)}Enclosing function: the function containing the ? must return the same fallible enum, a Result propagates only out of a -> Result<_, _> function, an Option only out of a -> Option<_> one. Otherwise the failure has nowhere to go:
error: `?` on a Option<i32> has nowhere to propagate: the enclosing function returns i32
No conversion: the error travels as-is. There is no From/Into trait system, so the callee's E must already be the caller's E; a mismatch is an ordinary type error. Convert first with .map_err(...) when the types differ.
Payload types are independent: only the error types must agree. ? on a Result<bool, E> inside a -> Result<i32, E> function is fine, the unwrapped bool is used locally, not returned.
Short-circuiting: nothing after a failing ? runs, including the rest of a loop body, ? returns from the function, not from the iteration.
Precedence: postfix, binding as tightly as a call or index. f(x)? + 1 adds to the unwrapped payload, and parse(s)?.field reads a field of the unwrapped value.
Runnable program: examples/operators/error_propagation.nr.
When the reason for a failure should be handled rather than forwarded, use match, ?? for a fallback, or val-else to unwrap or leave the scope.
Operator Precedence
From highest to lowest, matching the parser's Pratt ladder:
| Level | Operators | Associativity | Example |
|---|---|---|---|
| 16 (highest) | . | L-to-R | p.x |
| 15 | call f(…), index a[i], postfix ?, turbofish ::<…> | L-to-R | f(x)?, arr[i] |
| 14 | - (unary), !, ~ | R-to-L | -x, !flag, ~mask |
| 13 | as | L-to-R | n as f64 |
| 12 | *, /, % | L-to-R | a * b, n % 2 |
| 11 | +, - | L-to-R | a + b, x - y |
| 10 | << | L-to-R | a << 4 |
| 9 | <, >, <=, >= | L-to-R | x < y |
| 8 | ==, != | L-to-R | x == y |
| 7 | & | L-to-R | a & mask |
| 6 | ^ | L-to-R | a ^ b |
| 5 | | | L-to-R | a | b |
| 4 | && | L-to-R | a && b |
| 3 | || | L-to-R | a || b |
| 2 | ?? | R-to-L | a ?? b ?? c parses as a ?? (b ?? c) |
| 1 (lowest) | .., ..= | L-to-R | 1..=n |
Comparison binds tighter than equality: x < y == z parses as (x < y) == z. There is no
>> operator; right shift is the .shr(n) method because >> is reserved for function
composition. |
Precedence Examples
a + b * c // Same as: a + (b * c)a * b + c // Same as: (a * b) + ca < b == c < d // Same as: (a < b) == (c < d)!a && b // Same as: (!a) && ba || b && c // Same as: a || (b && c)Using Parentheses
(a + b) * c // Force addition firsta * (b + c) // Force addition before multiplication(a && b) || c // Force AND before OR (though same as default)Type Requirements
Numeric Operators
+, -, *, / work with:
i8,i16,i32,i64u8,u16,u32,u64f32,f64
Both operands must be the same type.
+ additionally works on string (and &string) as concatenation, producing a new owned
string. The other arithmetic operators have no string meaning.
Integer-Only Operators
%, &, |, ^, ~, << work only with integer types:
i8,i16,i32,i64u8,u16,u32,u64
Comparison Operators
==, !=, <, >, <=, >= work with:
- All numeric types (same type required)
- Note: Float comparison (
f32,f64) utilizes native IEEE-754 ordered predicates. Comparisons involvingNaNwill naturally returnfalse.
- Note: Float comparison (
bool(only==and!=)char, which has a built-in total order over its Unicode scalar valuesstring(only==and!=), byte-level equality via length check +memcmp- a newtype over any of the above, which compares as its inner type does
Nothing else has comparison built in. A struct gets == / != from impl PartialEq and
the ordering operators from impl Comparable (see Operator Overloading);
comparing one that implements neither is a type error naming the missing trait. Arrays,
tuples, enums and collections have no equality at all yet, and neither does a reference to
anything but a string — read through it with * first.
Logical Operators
&&, ||, ! work only with bool
Common Patterns
Range Checking
val in_range: bool = x >= min && x <= maxval out_of_range: bool = x < min || x > maxClamping
val clamped: i32 = if x < min { min } else if x > max { max } else { x }Sign Determination
val sign: i32 = if x > 0 { 1 } else if x < 0 { -1 } else { 0 }Absolute Value
val abs: i32 = if x >= 0 { x } else { -x }Common Mistakes
Type Mismatch
val x: i32 = 10val y: f64 = 3.14// val z = x + y // Error: cannot add i32 and f64Integer Division
val result: i32 = 5 / 2 // Result is 2, not 2.5Use floats for decimal division:
val result: f64 = 5.0 / 2.0 // Result is 2.5Boolean Comparison
val flag: bool = true// if flag == true { } // Redundantif flag { } // BetterOperator Overloading
Operators on a custom type are sugar for method calls. Implement the corresponding
operator trait to make an operator work on your type. The operator traits are
built into the compiler: you write only the impl, never a trait declaration.
An arithmetic, bitwise, or unary operator trait declares its result type with
type Output = T:
@derive(Copy, Clone)struct Vec2 { x: i32, y: i32 } impl Add for Vec2 { type Output = Vec2 func add(self, rhs: Vec2) -> Vec2 { Vec2 { x: self.x + rhs.x, y: self.y + rhs.y } }} impl Neg for Vec2 { type Output = Vec2 func neg(self) -> Vec2 { Vec2 { x: -self.x, y: -self.y } }} val c = Vec2 { x: 1, y: 2 } + Vec2 { x: 3, y: 4 } // (4, 6), via Add::addval d = -c // (-4, -6), via Neg::negComparison uses PartialEq (equality) and Comparable (ordering); their methods take
&self and rhs: &Self and return bool. Comparable requires PartialEq on the
same type:
impl PartialEq for Vec2 { func eq(&self, rhs: &Vec2) -> bool { self.x == rhs.x && self.y == rhs.y } func ne(&self, rhs: &Vec2) -> bool { self.x != rhs.x || self.y != rhs.y }} if Vec2 { x: 1, y: 2 } == Vec2 { x: 1, y: 2 } { } // via PartialEq::eqOperator → trait → method:
| Operator(s) | Trait | Method(s) |
|---|---|---|
+ | Add | add |
- (binary) | Sub | sub |
* | Mul | mul |
/ | Div | div |
% | Rem | rem |
-a | Neg | neg |
~a | Not | not |
& | ^ << | BitAnd BitOr BitXor Shl | bitand bitor bitxor shl |
== != | PartialEq | eq ne |
< <= > >= | Comparable | lt le gt ge |
Rules and limits:
- The receiver type must be
Copy(the scalar path). Each operator dispatches to its own method; implement the method for every operator you use. - A declared
type Outputmust match the method's return type. - The logical
!a(boolean NOT) is not overloadable: it is always boolean negation. - Compound assignment (
v += w) works when the type implements the matching by-value operator: it desugars tov = v + w. Dedicated in-place*Assigntraits, matrix multiply@, and auto-derived comparison defaults are planned for later phases. - Operator overloading is fully monomorphized and erased: each operator becomes the method call it stands for, with no vtable and no runtime cost.
See examples/operators/operator_overloading.nr
for a complete program.
References
- Types - Type requirements for operators
- Expressions - Operator precedence and evaluation
- Variables - Assignment operator