Skip to main content

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.

LevelOperatorsNotes
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
14asExplicit cast (binds tighter than arithmetic and logical operators above it)
15unary ! + - ~ * &, tryPrefix operators
16catchApplies to the left expression, supports catch (err) expr / catch (err) { ... }
17call () member access . ?. indexing []Highest precedence

Precedence Notes

  • Function calls, member access, and indexing bind the tightest.
  • Assignment is right-associative (a = b = 3 assigns to b first, then a).
  • as applies before arithmetic and logical operators listed above it in the table.
  • try is a prefix expression operator and participates in precedence like other unary operators.
  • catch applies 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:

TypeBehavior
Integers, floats, booleansValue comparison
strByte-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 / enumsSupported 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:

  • struct methods
  • interface method 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 (self only)
  • 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.

See also