Operators
Zynx expressions follow a fixed precedence order, and you can overload many of these operators for your own types.
Operator Precedence
Zynx expressions follow a fixed precedence order from lowest to highest.
This section describes that order, explains how to read the table, and shows examples for common patterns.
Precedence Table
The table below lists operators from lowest precedence (evaluated last) at level 1 to highest precedence (evaluated first) at level 17.
| Level | Operators | Notes |
|---|---|---|
| 1 | = += -= *= /= %= <<= >>= &= |= ^= | Assignment, right-associative |
| 2 | ?: | Ternary conditional |
| 3 | || | Logical OR |
| 4 | && | Logical AND |
| 5 | | | Bitwise OR |
| 6 | ^ | Bitwise XOR |
| 7 | & | Bitwise AND |
| 8 | == != | Equality |
| 9 | < > <= >= | Comparison |
| 10 | << >> | Shift |
| 11 | + - | Addition and subtraction |
| 12 | ... ..< | Inclusive and exclusive ranges |
| 13 | * / % | Multiplication, division, remainder |
| 14 | as | Explicit cast (binds tighter than arithmetic and logical operators above it) |
| 15 | unary ! + - ~ * &, try | Prefix operators |
| 16 | catch | Applies to the left expression, supports catch (err) expr / catch (err) { ... } |
| 17 | call () member access . ?. indexing [] | Highest precedence |
Precedence Notes
- Function calls, member access, and indexing bind the tightest.
- Assignment is right-associative (
a = b = 3assigns tobfirst, thena). asapplies before arithmetic and logical operators listed above it in the table.tryis a prefix expression operator and participates in precedence like other unary operators.catchapplies to the expression on its left and binds more tightly than surrounding binary operators.- Capture form is valid in both block and expression handlers:
expr catch (err) fallback_expr.
In practice:
- Use precedence to predict how complex expressions will be parsed.
- Rely on parentheses whenever there is any ambiguity for a human reader, even if the compiler would interpret the expression correctly without them.
Examples
let x = a + b * c;
// parsed as: a + (b * c)
let y = foo().bar[i] + 1;
// parsed as: ((foo()).bar[i]) + 1
let z = value catch 0 + 1;
// parsed as: (value catch 0) + 1
let w = a = b = 3;
// parsed as: a = (b = 3)
A few additional patterns:
// `as` applies before `*`
let v = a as f64 * 2.0;
// parsed as: (a as f64) * 2.0
// `try` applies before `+`
let total = try compute() + 1;
// parsed as: (try compute()) + 1
// Function call and member access bind tighter than `catch`
let out = value.catch_default().field + 1;
// parsed as: ((value.catch_default()).field) + 1
Equality Operators (==, !=)
The == and != operators are supported on the following types:
| Type | Behavior |
|---|---|
| Integers, floats, booleans | Value comparison |
str | Byte-wise string comparison (equivalent to strcmp) |
Pointers (*T) | Address comparison |
References (&T, nullable ?T) | Address comparison |
Sized arrays (T[N]) | Element-wise memory comparison (equivalent to memcmp) |
| Structs / enums | Supported if == / != are overloaded |
Array equality example
Two sized arrays of the same type and length can be compared directly:
let a = [1, 2, 3];
let b = [1, 2, 3];
let c = [1, 2, 4];
a == b // true — all elements match
a != c // true — last element differs
Both sides must have the same element type and the same compile-time length; a type mismatch is a compile error.
Operator Overloading
Zynx lets you overload operators for your own types by declaring methods whose names are operator tokens.
This is supported on:
structmethodsinterfacemethod requirements (so generic code can use operators through bounds)
Basic Overloading Syntax
Define operator methods directly in a struct body:
struct Vec2 {
x: i32,
y: i32,
+(self, other: const &Vec2) -> Vec2 {
return Vec2 { x: self.x + other.x, y: self.y + other.y };
}
}
fn main() {
let a = Vec2 { x: 1, y: 2 };
let b = Vec2 { x: 3, y: 4 };
let c = a + b;
}
For interfaces, use the same operator form as method signatures:
interface Addable<T> {
+(self, other: const &T) -> T;
}
Supported Operator Methods
Commonly used overloadable operators include:
- Arithmetic:
+-*/% - Bitwise:
&|^~<<>> - Comparison:
==!=<><=>= - Logical not:
! - Assignment variants:
=+=-=*=/=%=&=|=^=<<=>>= - Indexing:
[]
You can also overload unary forms such as unary - and unary + by defining a one-parameter operator method.
Unary vs Binary Operators
Some symbols (+, -, *, &) can be unary or binary.
- Unary overload: one parameter (
selfonly) - Binary overload: two parameters (
self+ operand)
Example:
struct Point {
x: i32,
y: i32,
-(self) -> Point {
return Point { x: -self.x, y: -self.y };
}
+(self, other: const &Point) -> Point {
return Point { x: self.x + other.x, y: self.y + other.y };
}
}
Mutating Assignment Operators
Assignment-style operators (like +=, -=, *=, etc.) are mutating operators and are intended to modify self.
struct Counter {
val: i32,
+=(self, other: i32) {
self.val += other;
}
}
Generic Use Through Interfaces
Operator overloading is especially useful with interface constraints:
interface Addable<T> {
+(self, other: const &T) -> T;
}
fn add_any<T: Addable<T>>(a: T, b: T) -> T {
return a + b;
}
This allows generic algorithms to use operators without knowing concrete types.
Overloading Notes
- Operator precedence is unchanged by overloading; only behavior is customized.
- Overloading resolution follows the same candidate-selection rules as regular method/function calls.