Operators

Built-in operators, precedence, assignment forms, and overloadable operator methods.

Zynx parses expressions by precedence. Higher-precedence operators bind first; operators at the same level use their associativity. Use parentheses when a reader might have to remember the table.

fn main() {
    let shifted = 1 + 2 * 3 << 1;
    assert shifted == 14;

    let nested = false ? 1 : true ? 2 : 3;
    assert nested == 2;

    let flags = 0b1100;
    let mask = 0b1000;
    assert (flags & mask) == mask;
}

Precedence

From highest to lowest:

LevelOperatorsAssociativity
Postfix access(), [], ., ?.left
Prefix, cast, propagation!x, +x, -x, ~x, *x, &x, x as T, try x, await xprefix / as left
Multiplicative*, /, %left
Ranges..., ..<left
Additive+, -left
Shifts<<, >>left
Relational<, >, <=, >=left
Equality==, !=left
Bitwise and&left
Bitwise xor^left
Bitwise or|left
Logical and&&left
Logical or, catch, ternary||, catch, ? :|| and catch left, ternary right
Assignment=, :=, +=, -=, *=, /=, %=, <<=, >>=, &=, |=, ^=right

Some precedence choices are worth remembering:

  • Ranges bind tighter than + and -. Write 0..<(limit + 1) when an endpoint is an additive expression.
  • Equality binds tighter than bitwise operators. Write (flags & mask) == mask for bit-mask checks.
  • catch, ||, and ternary share one low-precedence level. Add parentheses when combining them.
  • as, try, and await bind like prefix operators. Parenthesize when they should cover a larger expression.

For the full evaluation-order contract, see Evaluation Order.

Numeric Operators

Arithmetic operators work on matching numeric scalar types and matching vector types. Literal operands can coerce when the literal fits the other operand's type.

fn main() {
    let a: i32 = 10;
    let b: i32 = 3;

    assert a + b == 13;
    assert a - b == 7;
    assert a * b == 30;
    assert a / b == 3;
    assert a % b == 1;
}

Integer +, -, *, unary -, and compound assignment forms trap on overflow or underflow in both debug and release builds. Division and remainder trap on zero divisors and signed minimum divided by -1. Use std.arith helpers when wrapping, saturating, overflowing, or checked behavior should be explicit at the call site.

Pointer arithmetic is limited to pointer-plus-integer, integer-plus-pointer, pointer-minus-integer, and same-type pointer-minus-pointer. Pointer difference returns isize. Raw pointer arithmetic and dereference require unsafe context.

fn main() {
    let values: [i32] = [10, 20, 30];
    let base: *i32 = &values[0] as *i32;
    let third: *i32 = null;
    let distance: isize = 0;

    unsafe {
        third = base + 2;
        distance = third - base;
        assert *third == 30;
    }

    assert distance == 2;
}

Bitwise And Logical

Bitwise operators require integer operands of matching type.

fn main() {
    let flags = 0b1010;
    let mask = 0b1100;

    assert (flags & mask) == 0b1000;
    assert (flags | mask) == 0b1110;
    assert (flags ^ mask) == 0b0110;
    assert (1 << 3) == 8;
    assert (16 >> 2) == 4;
}

For built-in integer shifts, signed >> is arithmetic shift and unsigned >> is logical shift.

Logical && and || short-circuit and return bool. Prefix ! returns bool.

fn main() {
    let ready = true;
    let stopped = false;

    assert ready && !stopped;
    assert ready || stopped;
}

Comparison

Comparison operators return bool.

Relational comparisons <, <=, >, and >= are for numeric values and raw pointers. Equality == and != is broader: it supports numeric values, str, raw pointers, references, nullable values, null, callable values, and array values where the element type can be compared.

Vector comparisons also return one bool, true only when every lane satisfies the comparison. See SIMD Vectors.

Casts

as supports numeric value casts, pointer-to-pointer casts, pointer <-> usize or pointer <-> isize casts, and explicit user-defined cast(self) methods.

fn main() {
    let byte: u8 = 42;
    let wide: i32 = byte as i32;

    assert wide == 42;
}

as supports explicit reference-to-pointer casts such as &value as *T. It does not cast raw pointers back to checked references. Use references for checked borrows and raw pointers only for interop or manual memory.

Structs can expose explicit casts by defining one or more cast(self) methods. Overloads are selected by return type, so the target after as chooses the method. Same-target cast overloads are ambiguous.

error CastError {
    Invalid
}

struct Point {
    x: i32,
    y: i32,

    fn cast(self) -> i32 {
        return self.x + self.y;
    }

    fn cast(self) -> str {
        return "point";
    }
}

struct FalliblePoint {
    ok: bool,

    fn cast(self) throws(CastError) -> Point {
        if !self.ok {
            throw CastError.Invalid;
        }
        return Point { x: 7, y: 11 };
    }
}

fn main() {
    let point = Point { x: 2, y: 3 };

    assert (point as i32) == 5;
    assert (point as str) == "point";

    let converted = try (FalliblePoint { ok: true } as Point);
    assert converted.x == 7;
}

User-defined casts are explicit only. They do not apply to assignment, function arguments, returns, or conditionals unless the source expression uses as.

User-defined casts are separate from standard-library view helpers. Current stdlib view APIs use named accessors and constructors such as text.str(), text.utf8(), array.slice(), stream.bytes(), bytes.Bytes(value), and crypto wrapper bytes() methods.

Assignment

Assignment targets are variables, fields, subscript results, dereferenced references or pointers, and calls that return references. Compound assignments read the old value, apply the matching binary operator, then write the result. Condition binding name := expression is accepted only as the whole condition of if or while.

fn main() {
    let value = 1;
    value += 2;

    let items = [1, 2];
    items[1] = 7;

    assert value == 3;
    assert items[1] == 7;
}

_ = expr discards a value. Error unions cannot be discarded; handle them with try or catch.

Ranges And Ternary

..< creates an exclusive range, and ... creates an inclusive range. Range operands must be integer values. Ranges are most commonly used with for.

fn main() {
    let sum = 0;

    for value in 1...3 {
        sum += value;
    }

    assert sum == 6;
}

The ternary operator chooses between two expressions:

fn main() {
    let count = 0;
    let label = count == 0 ? "empty" : "ready";

    assert label == "empty";
}

The condition must be a plain integer/bool-like value. Error-union values must be handled with try or catch before they are used as conditions. The two branches must have the same type, compatible numeric types, or a common callable type.

Error Flow

try unwraps an error union or returns the error from the current function. catch handles an error union and supplies a fallback value.

error ParseError {
    Empty
}

fn parse(text: str) throws(ParseError) -> i32 {
    if text == "" {
        throw ParseError.Empty;
    }
    return 42;
}

fn main() {
    let fallback = parse("") catch { _ => 0 };
    assert fallback == 0;
}

See Error Handling for catch blocks, error bindings, and try await.

Operator Overloading

Structs and interfaces can declare operator methods. The method name is the operator token, and the usual operator precedence still applies at call sites.

struct Offset {
    value: i32,

    +(self, other: const &Offset) -> Offset {
        return Offset { value: self.value + other.value };
    }

    [](self, index: i32) -> i32 {
        return self.value + index;
    }

    []=(self, index: i32, value: i32) {
        self.value = value - index;
    }
}

fn main() {
    let start = Offset { value: 5 };
    let next = Offset { value: 7 };
    let sum = start + next;

    assert sum.value == 12;
    assert start[2] == 7;

    start[2] = 20;
    assert start.value == 18;
}

Interfaces can require operator support:

interface Addable<T> {
    +(self, other: const &T) -> T;
}

Overloadable source forms are unary +, unary -, !, ~, binary arithmetic/bitwise/shift operators, comparisons, subscript [], and plain indexed assignment []=. Compound assignment forms such as +=, logical short-circuit operators && and ||, call (), ranges, casts, address-of, raw dereference, try, catch, await, member access, optional member access, and ternary are not user-overloadable.

Operator methods use the same overload resolution machinery as functions and methods, but operator lookup uses only the left operand or unary receiver. See Function Overloading and Interfaces.

For container[index] = rhs, built-in array, pointer, tuple, and vector assignment is tried first. If that is not a built-in assignment target, the compiler searches for container.[]=(index, rhs). Only plain = rewrites this way; container[index] += rhs does not call []=.