Skip to content

Operator Overloading

Vex supports operator overloading through contracts and operator methods. This allows custom types to use natural operators like +, -, *, ==, and [].

Current Status

Operator overloading is implemented and exercised by repository regression tests, including cross-type operator cases and multi-RHS operator dispatch.

Overview

Operator overloading in Vex uses a contract-based system:

  1. Define or use a contract with an operator method
  2. Implement the contract on your struct
  3. Use the operator naturally in code
vex
// 1. Contract defines the operator
contract Add {
    op+(other: Self): Self;
}

// 2. Struct implements the contract
struct Point:Add {
    x: i32,
    y: i32,
    
    fn op+(other: Point): Point {
        Point { x: self.x + other.x, y: self.y + other.y }
    }
}

// 3. Use the operator
fn main(): i32 {
    let p1 = Point { x: 1, y: 2 }
    let p2 = Point { x: 3, y: 4 }
    let p3 = p1 + p2  // Calls op+
    return 0
}

Standard Operator Contracts

Vex provides built-in contracts for all operators. They use the $ prefix.

Arithmetic Operators

OperatorContractMethodExample
a + b$Addop+(rhs: Self): SelfAddition
a - b$Subop-(rhs: Self): SelfSubtraction
a * b$Mulop*(rhs: Self): SelfMultiplication
a / b$Divop/(rhs: Self): SelfDivision
a % b$Modop%(rhs: Self): SelfModulo
-a$Negop-(): SelfNegation
vex
struct Vec2:$Add, $Sub, $Neg {
    x: f64,
    y: f64,
    
    fn op+(other: Vec2): Vec2 {
        Vec2 { x: self.x + other.x, y: self.y + other.y }
    }
    
    fn op-(other: Vec2): Vec2 {
        Vec2 { x: self.x - other.x, y: self.y - other.y }
    }
    
    fn op-(): Vec2 {
        Vec2 { x: -self.x, y: -self.y }
    }
}

let v = Vec2 { x: 1.0, y: 2.0 }
let neg = -v  // Vec2 { x: -1.0, y: -2.0 }

Comparison Operators

OperatorContractMethodExample
a == b$Eqop==(rhs: Self): boolEquality
a != b$Eqop!=(rhs: Self): boolInequality
a < b$Ordop<(rhs: Self): boolLess than
a <= b$Ordop<=(rhs: Self): boolLess or equal
a > b$Ordop>(rhs: Self): boolGreater than
a >= b$Ordop>=(rhs: Self): boolGreater or equal
vex
struct Version:$Eq, $Ord {
    major: i32,
    minor: i32,
    
    fn op==(other: Version): bool {
        self.major == other.major && self.minor == other.minor
    }
    
    fn op!=(other: Version): bool {
        !(self == other)
    }
    
    fn op<(other: Version): bool {
        if self.major != other.major {
            return self.major < other.major
        }
        self.minor < other.minor
    }
    
    fn op<=(other: Version): bool { self < other || self == other }
    fn op>(other: Version): bool { !(self <= other) }
    fn op>=(other: Version): bool { !(self < other) }
}

Bitwise Operators

OperatorContractMethodExample
a & b$BitAndop&(rhs: Self): SelfAND
a | b$BitOrop|(rhs: Self): SelfOR
a ^ b$BitXorop^(rhs: Self): SelfXOR
~a$BitNotop~(): SelfNOT
a << n$Shlop<<(rhs: i32): SelfLeft shift
a >> n$Shrop>>(rhs: i32): SelfRight shift
vex
struct Flags:$BitOr, $BitAnd {
    value: u32,
    
    fn op|(other: Flags): Flags {
        Flags { value: self.value | other.value }
    }
    
    fn op&(other: Flags): Flags {
        Flags { value: self.value & other.value }
    }
}

const READ = Flags { value: 1 }
const WRITE = Flags { value: 2 }
let perms = READ | WRITE  // Flags { value: 3 }

Index Operators

OperatorContractMethodExample
a[i]$Indexop[](index: Idx): OutputRead access
a[i] = v$IndexMutop[]=(index: Idx, value: Val)Write access
a[i..j]$Sliceop[..](start, end): OutputSlice read
vex
struct Matrix:$Index, $IndexMut {
    type Output = f64;
    data: Vec<f64>,
    cols: i64,
    
    fn op[](index: i64): f64 {
        self.data.get(index)
    }
    
    fn op[]=(index: i64, value: f64) {
        // Set value at index
    }
}

let m = Matrix { ... }
let val = m[5]     // Calls op[]
m[5] = 3.14        // Calls op[]=

Compound Assignment

OperatorContractMethod
a += b$AddAssignop+=(rhs: Self)
a -= b$SubAssignop-=(rhs: Self)
a *= b$MulAssignop*=(rhs: Self)
a /= b$DivAssignop/=(rhs: Self)
a %= b$ModAssignop%=(rhs: Self)
a &= b$BitAndAssignop&=(rhs: Self)
a |= b$BitOrAssignop|=(rhs: Self)
a ^= b$BitXorAssignop^=(rhs: Self)
a <<= n$ShlAssignop<<=(rhs: i32)
a >>= n$ShrAssignop>>=(rhs: i32)
vex
struct Counter:$AddAssign {
    value: i32,
    
    fn op+=(amount: Counter) {
        self.value = self.value + amount.value
    }
}

let! c = Counter { value: 10 }
c += Counter { value: 5 }  // c.value is now 15

Advanced Operators

OperatorContractMethodExample
a ** b$Powop**(exp: i32): SelfPower
++a$PreIncop++(): SelfPre-increment
a++$PostIncop++(): SelfPost-increment
--a$PreDecop--(): SelfPre-decrement
a--$PostDecop--(): SelfPost-decrement
a..b$Rangeop..(end): Range<Self>Range
a..=b$RangeInclusiveop..=(end): RangeInclusive<Self>Inclusive range
a ?? b$NullCoalesceop??(fallback): SelfNull coalesce

External Operator Methods

You can also define operators outside the struct using method syntax:

vex
struct Vector2 {
    x: f64,
    y: f64,
}

// External operator method
fn (self: Vector2) op+(other: Vector2): Vector2 {
    Vector2 {
        x: self.x + other.x,
        y: self.y + other.y,
    }
}

// Works the same
let v1 = Vector2 { x: 1.0, y: 2.0 }
let v2 = Vector2 { x: 3.0, y: 4.0 }
let v3 = v1 + v2

Custom Contracts

Define your own contracts for domain-specific operators:

vex
// Custom scalar multiplication contract
contract ScalarMul {
    mul_scalar(scalar: f64): Self;
}

struct Vec3:ScalarMul {
    x: f64,
    y: f64,
    z: f64,
    
    fn mul_scalar(scalar: f64): Vec3 {
        Vec3 {
            x: self.x * scalar,
            y: self.y * scalar,
            z: self.z * scalar,
        }
    }
}

let v = Vec3 { x: 1.0, y: 2.0, z: 3.0 }
let scaled = v.mul_scalar(2.5)

Current Implementation Notes

  • Operator overload resolution prefers exact matches before broader compatible numeric matches.
  • Multi-RHS overloads such as Vec + Vec and Vec + i32 are supported and regression-tested.
  • Unconstrained integer literals can still behave differently inside overload resolution than a plain annotated variable would; if you need a specific overload, prefer an explicit cast such as (10 as i32).

Tested Scenarios

ScenarioStatusNotes
Same-type arithmetic operatorsCovered by operator regression tests
Cross-type RHS operator dispatchCovered by operator_default_rhs_001.vx
Exact-match numeric preferenceCovered by numeric_exact_001.vx
Complex user-defined operator contractsCovered by tests/07_contracts/operators/complex_arith_001.vx
Exhaustive generic/default/variadic interaction with operators⚠️ PartialOperator core is stable, but the full interaction matrix is not yet exhaustive

Scope Note

The operator overloading core is solid, but the repository does not yet claim exhaustive coverage for every overload interaction involving generic constraints, variadics, and default parameters.

Repository Maturity Note

Overloading is in good shape, but the broader repository is still under active development. For example, docs/specs/LANGUAGE_SPEC.md still marks several contract-related features as partial, and docs/planning/VEX_TYPE_SYSTEM_CONTRACT_PHASES.md explicitly notes that generic bound solving remains partial.

Implementing Multiple Contracts

A single struct can implement multiple operator contracts:

vex
struct Complex:$Add, $Sub, $Mul, $Eq, $Display {
    real: f64,
    imag: f64,
    
    fn op+(other: Complex): Complex {
        Complex { real: self.real + other.real, imag: self.imag + other.imag }
    }
    
    fn op-(other: Complex): Complex {
        Complex { real: self.real - other.real, imag: self.imag - other.imag }
    }
    
    fn op*(other: Complex): Complex {
        // (a + bi)(c + di) = (ac - bd) + (ad + bc)i
        Complex {
            real: self.real * other.real - self.imag * other.imag,
            imag: self.real * other.imag + self.imag * other.real,
        }
    }
    
    fn op==(other: Complex): bool {
        self.real == other.real && self.imag == other.imag
    }
    
    fn op!=(other: Complex): bool {
        !(self == other)
    }
    
    fn display(): string {
        // Format as "a + bi"
        return "Complex"
    }
}

Non-Overloadable Operators

The following operators cannot be overloaded:

OperatorReason
&&Short-circuit evaluation
||Short-circuit evaluation
=Assignment semantics
.Member access
?.Optional chaining

Best Practices

  1. Follow semantics - op+ should behave like addition
  2. Implement related operators - If op==, also implement op!=
  3. Return Self - Arithmetic operators should return Self type
  4. Don't surprise - Operators should be intuitive for users
  5. Use contracts - They provide compile-time checking

Example: Matrix Type

vex
struct Matrix:$Add, $Mul, $Index, $Eq {
    type Output = f64;
    data: Vec<f64>,
    rows: i64,
    cols: i64,
    
    fn op+(other: Matrix): Matrix {
        $assert(self.rows == other.rows && self.cols == other.cols)
        let! result = Vec.with_capacity<f64>(self.data.len())
        for i in 0..self.data.len() {
            result.push(self.data.get(i) + other.data.get(i))
        }
        Matrix { data: result, rows: self.rows, cols: self.cols }
    }
    
    fn op*(other: Matrix): Matrix {
        $assert(self.cols == other.rows)
        // Matrix multiplication implementation
        // ...
    }
    
    fn op[](index: i64): f64 {
        self.data.get(index)
    }
    
    fn op==(other: Matrix): bool {
        if self.rows != other.rows || self.cols != other.cols {
            return false
        }
        for i in 0..self.data.len() {
            if self.data.get(i) != other.data.get(i) {
                return false
            }
        }
        true
    }
    
    fn op!=(other: Matrix): bool {
        !(self == other)
    }
}

fn main(): i32 {
    let a = Matrix { ... }
    let b = Matrix { ... }
    let c = a + b       // Matrix addition
    let d = a * b       // Matrix multiplication
    let val = c[0]      // Index access
    let eq = a == b     // Equality check
    return 0
}

Released under the MIT License.