WEP: Operator Overloading
Context
Wado needs a design for operator overloading to support custom types with operators. This is particularly important for:
- SIMD types: Vector types like
Vec3,Vec4,Mat4need arithmetic operators - Custom numeric types: BigInt, Complex, Rational numbers
- Index access: Custom containers need
[]operator - Display: Custom string representation (already handled by Display trait)
- Equality: Custom equality semantics (though field-by-field comparison usually suffices)
Language Survey
Different languages take different approaches:
Rust: Trait-Based
Rust uses traits for operator overloading. Each operator has a corresponding trait (e.g., Add, Sub, Mul):
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point { x: self.x + other.x, y: self.y + other.y }
}
}
Pros:
- Explicit and discoverable
- Type-safe with associated types
- Integrates with trait system
- Generic code can bound on operator traits
Cons:
- Verbose for simple cases
- Need to import traits to use operators
C++: operator Keyword
C++ uses special operator function names with flexible syntax (member or friend):
Point operator+(const Point& other) const {
return Point{x + other.x, y + other.y};
}
Pros:
- Compact syntax
- Flexible (member vs. friend functions)
Cons:
- Complex lookup rules
- Can be member or non-member (confusing)
- No trait-like abstraction
Kotlin: operator Modifier
Kotlin uses the operator keyword modifier on predefined function names:
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
Pros:
- Clear opt-in with
operatorkeyword - Fixed function names prevent abuse
Cons:
- No trait abstraction for generic code
- Less discoverable than explicit traits
Swift: Protocol-Based
Swift uses protocols (similar to traits) with static functions:
extension Point: Equatable {
static func == (lhs: Point, rhs: Point) -> Bool {
return lhs.x == rhs.x && lhs.y == rhs.y
}
}
Pros:
- Protocol-oriented
- Type-safe
Cons:
- Must be static functions (not methods)
- Syntax can be awkward
Zig: No Operator Overloading
Zig explicitly rejects operator overloading to avoid "hidden control flow":
// Zig - explicit function calls only
const sum = vec_add(a, b); // Not: a + b
Pros:
- No surprises, all control flow explicit
- Simple implementation
Cons:
- Verbose for math-heavy code
- Poor ergonomics for SIMD/vector math
- Community repeatedly requests this feature
Python: Dunder Methods
Python uses special "dunder" (double underscore) methods:
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
Pros:
- Simple and clear
- Well-established convention
Cons:
- No compile-time checking
- Not type-safe
- Verbose naming
Design Goals
- Consistency: Align with Wado's existing trait system
- Discoverability: Clear which operators are overloaded
- Safety: Prevent abuse and surprising behavior
- Ergonomics: Good experience for math-heavy code (SIMD)
- Wasm GC compatibility: Map to efficient Wasm code
Implementation Status
IMPORTANT: The trait system itself is not yet implemented in Wado. This WEP documents the design for operator overloading that will be implemented once the trait system is in place.
The trait syntax and semantics will follow Rust's design closely (as documented in wep-2026-01-13-struct-and-trait.md). This WEP extends that design to specify which traits correspond to which operators.
Decision
1. Use Trait-Based Operator Overloading (Rust Style)
Wado adopts Rust's trait-based approach for operator overloading. Each operator maps to a trait with a specific method name.
Rationale:
- Consistent with Wado's existing trait system (see
wep-2026-01-13-struct-and-trait.md) - Explicit and type-safe
- Enables generic programming with operator bounds
- Well-proven design
- Familiar to Rust developers
2. Overloadable Operators
Not all operators should be overloadable. Here's what Wado allows:
Arithmetic Operators (Essential for SIMD)
trait Add {
type Output;
fn add(self, rhs: Self) -> Self::Output;
}
trait Sub {
type Output;
fn sub(self, rhs: Self) -> Self::Output;
}
trait Mul {
type Output;
fn mul(self, rhs: Self) -> Self::Output;
}
trait Div {
type Output;
fn div(self, rhs: Self) -> Self::Output;
}
trait Rem {
type Output;
fn rem(self, rhs: Self) -> Self::Output;
}
Usage:
impl Add for Vec3 {
type Output = Vec3;
fn add(self, other: Vec3) -> Vec3 {
return Vec3 {
x: self.x + other.x,
y: self.y + other.y,
z: self.z + other.z,
};
}
}
let v1 = Vec3 { x: 1.0, y: 2.0, z: 3.0 };
let v2 = Vec3 { x: 4.0, y: 5.0, z: 6.0 };
let v3 = v1 + v2; // Desugars to: v1.add(v2)
Note: self consumes the value (ownership transfer). For types that should preserve the original, implement Clone and clone before the operation, or take &self if the trait signature allows it.
Negation (Unary Minus)
trait Neg {
type Output;
fn neg(self) -> Self::Output;
}
Usage:
impl Neg for Vec3 {
type Output = Vec3;
fn neg(self) -> Vec3 {
return Vec3 {
x: -self.x,
y: -self.y,
z: -self.z,
};
}
}
let v = Vec3 { x: 1.0, y: 2.0, z: 3.0 };
let negated = -v; // Desugars to: v.neg()
Bitwise Operators
trait BitAnd {
type Output;
fn bitand(self, rhs: Self) -> Self::Output;
}
trait BitOr {
type Output;
fn bitor(self, rhs: Self) -> Self::Output;
}
trait BitXor {
type Output;
fn bitxor(self, rhs: Self) -> Self::Output;
}
trait BitNot {
type Output;
fn bitnot(self) -> Self::Output;
}
trait Shl {
type Output;
fn shl(self, rhs: u32) -> Self::Output;
}
trait Shr {
type Output;
fn shr(self, rhs: u32) -> Self::Output;
}
Use case: Custom bit flags, SIMD masks, etc.
Compound Assignment Operators (Essential for Efficiency)
Compound assignment operators enable in-place mutation, avoiding temporary object creation. This is especially important for String, List, and other types where cloning is expensive.
trait AddAssign<Rhs = Self> {
fn add_assign(&mut self, rhs: Rhs);
}
trait SubAssign<Rhs = Self> {
fn sub_assign(&mut self, rhs: Rhs);
}
trait MulAssign<Rhs = Self> {
fn mul_assign(&mut self, rhs: Rhs);
}
trait DivAssign<Rhs = Self> {
fn div_assign(&mut self, rhs: Rhs);
}
trait RemAssign<Rhs = Self> {
fn rem_assign(&mut self, rhs: Rhs);
}
trait BitAndAssign<Rhs = Self> {
fn bitand_assign(&mut self, rhs: Rhs);
}
trait BitOrAssign<Rhs = Self> {
fn bitor_assign(&mut self, rhs: Rhs);
}
trait BitXorAssign<Rhs = Self> {
fn bitxor_assign(&mut self, rhs: Rhs);
}
trait ShlAssign<Rhs = u32> {
fn shl_assign(&mut self, rhs: Rhs);
}
trait ShrAssign<Rhs = u32> {
fn shr_assign(&mut self, rhs: Rhs);
}
Usage:
impl AddAssign for Vec3 {
fn add_assign(&mut self, other: Vec3) {
self.x += other.x;
self.y += other.y;
self.z += other.z;
}
}
let mut v1 = Vec3 { x: 1.0, y: 2.0, z: 3.0 };
let v2 = Vec3 { x: 4.0, y: 5.0, z: 6.0 };
v1 += v2; // Desugars to: v1.add_assign(v2)
// v1 is now Vec3 { x: 5.0, y: 7.0, z: 9.0 }
Design notes:
- Takes
&mut selffor in-place mutation - RHS is taken by value (more flexible than by reference)
- No return value (
Outputtype) - always mutates in place - Useful for
Stringconcatenation,Listextension, numeric types
Example with String:
impl AddAssign for String {
fn add_assign(&mut self, other: String) {
self.push_str(&other); // Efficient in-place append
}
}
let mut s = "Hello";
s += ", World!"; // No temporary String created
Index Access (Essential for Containers)
trait Index<Idx> {
type Output;
fn index(&self, index: Idx) -> &Self::Output;
}
trait IndexMut<Idx> {
type Output;
fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}
Usage:
struct Matrix {
data: List<f64>,
rows: u32,
cols: u32,
}
impl Index<[u32, u32]> for Matrix {
type Output = f64;
fn index(&self, idx: [u32, u32]) -> &f64 {
let [row, col] = idx;
return &self.data[(row * self.cols + col) as usize];
}
}
impl IndexMut<[u32, u32]> for Matrix {
type Output = f64;
fn index_mut(&mut self, idx: [u32, u32]) -> &mut f64 {
let [row, col] = idx;
return &mut self.data[(row * self.cols + col) as usize];
}
}
let mut m = Matrix::new(3, 3);
m[[0, 0]] = 1.0; // Desugars to: *m.index_mut([0, 0]) = 1.0
let val = m[[0, 0]]; // Desugars to: *m.index([0, 0])
Equality and Comparison (Already Exist as Traits)
These traits already exist in Wado (see wep-2026-01-13-struct-and-trait.md):
trait Eq {
fn eq(&self, other: &Self) -> bool;
}
variant Ordering {
Less,
Equal,
Greater,
}
trait Ord {
fn cmp(&self, other: &Self) -> Ordering;
}
Design note: Most types should use the default field-by-field equality and comparison. Only override when custom semantics are needed (e.g., case-insensitive string comparison, floating-point with epsilon).
Display and Debug (Already Exist as Traits)
These traits already exist:
trait Display {
fn display(&self) -> String;
}
trait Debug {
fn debug(&self) -> String;
}
Usage:
Displayis for user-facing output, used in template strings:{value}Debugis for developer-facing output, used in debug printing
3. Non-Overloadable Operators
The following operators are NOT overloadable:
Logical Operators: &&, ||, !
Reason: These have short-circuit evaluation semantics that cannot be preserved with custom implementations. ! is logical NOT, not overloadable (bitwise NOT ~ is via BitNot trait).
let result = a && b; // ❌ Cannot overload
// Always short-circuits: if a is false, b is never evaluated
Assignment Operator: =
Reason: The basic assignment operator = has special semantics in Wado (value semantics, clone/move). It cannot be overloaded.
Note: Compound assignment operators (+=, -=, etc.) ARE overloadable via traits like AddAssign (see section above).
Range Operators: ..<, ..=
Reason: These construct RangeExclusive and RangeInclusive types, not user-overloadable.
Member Access: ., ::
Reason: Core language syntax, not overloadable.
Function Call: ()
Reason: User-defined callable types (structs that can be invoked with ()) are out of scope for this WEP. The internal Fn / FnMut traits used by closures (see Closure Implementation) are compiler-internal and not user-implementable in the MVP.
4. Operator Trait Naming Convention
Following Rust's convention:
| Operator | Trait | Method | Expression | Desugars to |
|---|---|---|---|---|
+ |
Add |
add |
a + b |
a.add(b) |
- |
Sub |
sub |
a - b |
a.sub(b) |
* |
Mul |
mul |
a * b |
a.mul(b) |
/ |
Div |
div |
a / b |
a.div(b) |
% |
Rem |
rem |
a % b |
a.rem(b) |
- (un) |
Neg |
neg |
-a |
a.neg() |
& |
BitAnd |
bitand |
a & b |
a.bitand(b) |
\| |
BitOr |
bitor |
a \| b |
a.bitor(b) |
^ |
BitXor |
bitxor |
a ^ b |
a.bitxor(b) |
~ |
BitNot |
bitnot |
~a |
a.bitnot() |
<< |
Shl |
shl |
a << b |
a.shl(b) |
>> |
Shr |
shr |
a >> b |
a.shr(b) |
[] |
Index |
index |
a[b] |
*a.index(b) |
[] (m) |
IndexMut |
index_mut |
a[b] = c |
*a.index_mut(b) |
== |
Eq |
eq |
a == b |
a.eq(&b) |
!= |
Eq |
(negated eq) |
a != b |
!a.eq(&b) |
< |
Ord |
cmp |
a < b |
a.cmp(&b) == Less |
<= |
Ord |
cmp |
a <= b |
a.cmp(&b) != Greater |
> |
Ord |
cmp |
a > b |
a.cmp(&b) == Greater |
>= |
Ord |
cmp |
a >= b |
a.cmp(&b) != Less |
+= |
AddAssign |
add_assign |
a += b |
a.add_assign(b) |
-= |
SubAssign |
sub_assign |
a -= b |
a.sub_assign(b) |
*= |
MulAssign |
mul_assign |
a *= b |
a.mul_assign(b) |
/= |
DivAssign |
div_assign |
a /= b |
a.div_assign(b) |
%= |
RemAssign |
rem_assign |
a %= b |
a.rem_assign(b) |
&= |
BitAndAssign |
bitand_assign |
a &= b |
a.bitand_assign(b) |
\|= |
BitOrAssign |
bitor_assign |
a \|= b |
a.bitor_assign(b) |
^= |
BitXorAssign |
bitxor_assign |
a ^= b |
a.bitxor_assign(b) |
<<= |
ShlAssign |
shl_assign |
a <<= b |
a.shl_assign(b) |
>>= |
ShrAssign |
shr_assign |
a >>= b |
a.shr_assign(b) |
Note: Method names use lowercase (e.g., bitand, not bit_and) for consistency with Rust.
5. Generic Programming with Operators
Traits enable generic functions with operator bounds:
// Generic add function
fn add_values<T: Add<Output = T>>(a: T, b: T) -> T {
return a + b;
}
// Works with any type that implements Add
let sum_i32 = add_values(1, 2); // i32
let sum_f64 = add_values(1.0, 2.0); // f64
let sum_vec = add_values(v1, v2); // Vec3
// Numeric trait bound (multiple operators)
fn dot_product<T>(a: Vec3<T>, b: Vec3<T>) -> T
where
T: Mul<Output = T> + Add<Output = T> + Clone,
{
return a.x.clone() * b.x.clone() +
a.y.clone() * b.y.clone() +
a.z * b.z;
}
6. Standard Library Implementations
Built-in types implement operator traits where appropriate:
// Primitives implement arithmetic
impl Add for i32 { ... }
impl Sub for i32 { ... }
// ... etc for i8, i16, i32, i64, u8, u16, u32, u64, f32, f64
// String concatenation (note: consider if this is desired)
impl Add for String {
type Output = String;
fn add(self, other: String) -> String {
return self.concat(other);
}
}
// List indexing
impl<T> Index<usize> for List<T> {
type Output = T;
fn index(&self, idx: usize) -> &T {
// bounds checking
return &self.data[idx];
}
}
impl<T> IndexMut<usize> for List<T> {
type Output = T;
fn index_mut(&mut self, idx: usize) -> &mut T {
// bounds checking
return &mut self.data[idx];
}
}
7. Restrictions and Guidelines
Do Not Overload Operators with Surprising Behavior
Operator implementations should match intuitive expectations:
// ❌ BAD: Confusing behavior
impl Add for User {
type Output = User;
fn add(self, other: User) -> User {
// Deletes both users from database?! 🤯
return User::deleted();
}
}
// ✅ GOOD: Use a named method for non-obvious operations
impl User {
pub fn merge(self, other: User) -> User {
// Clear intent
return User::merged(self, other);
}
}
Guidelines:
- Arithmetic operators (
+,-,*,/,%): Should behave like mathematical operations - Comparison operators (
<,>, etc.): Should define a meaningful ordering - Index operator (
[]): Should access elements, not perform unrelated operations - When in doubt, use a named method: If the operation is not obviously operator-like, use a method with a clear name
Operator Precedence Applies to Overloaded Operators
Overloaded operators follow the same precedence rules as built-in operators (see wep-2026-01-11-operator-precedence.md).
let result = a + b * c; // Always: a + (b * c), even if + and * are overloaded
Associated Type Flexibility
Operators can return different types via Output associated type:
// Scalar multiplication returns a vector
impl Mul<f64> for Vec3 {
type Output = Vec3;
fn mul(self, scalar: f64) -> Vec3 {
return 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 * 2.0; // Vec3
Consequences
Positive
- Consistency: Aligns with Wado's trait system
- Type safety: Associated types ensure type correctness
- Discoverability: Clear which types support which operators via traits
- Generic programming: Can write generic functions with operator bounds
- SIMD/math ergonomics: Natural syntax for vector/matrix math
- Familiar: Rust developers will recognize the pattern
- Explicit: Trait imports make dependencies clear
- Wasm mapping: Trait dispatch maps to Wasm vtables efficiently
Negative
- Verbosity: More verbose than C++ or Kotlin for simple cases
- Mitigation: Most users consume operator-overloaded types, not implement them
- No compound assignment: Cannot overload
+=,-=, etc.- Mitigation: Compiler can optimize
a = a + bto in-place operation - Alternative: Provide explicit
add_assign()methods when needed
- Mitigation: Compiler can optimize
- Trait import required: Need to import trait to use overloaded operators
- Mitigation: Standard traits in prelude (Add, Sub, etc.)
- Different from C++/Python: Developers from those languages need to learn traits
- Mitigation: Traits are a core Wado concept (already learned for other features)
Trade-offs
| Aspect | Rust/Wado (Traits) | C++/Kotlin (operator keyword) | Zig (No overloading) |
|---|---|---|---|
| Discoverability | ✅ High (via traits) | ⚠️ Medium (special syntax) | ✅ N/A |
| Type safety | ✅ High (associated types) | ⚠️ Medium | ✅ N/A |
| Generic programming | ✅ Yes (trait bounds) | ⚠️ Limited (templates) | ❌ No |
| Verbosity | ⚠️ Verbose | ✅ Concise | ✅ Explicit calls |
| Consistency | ✅ Part of trait system | ⚠️ Special case | ✅ No special cases |
| Ergonomics (math) | ✅ Good | ✅ Good | ❌ Poor |
Examples
Complete SIMD Vector Example
use {println} from "core:cli";
struct Vec3 {
x: f64,
y: f64,
z: f64,
}
impl Vec3 {
pub fn new(x: f64, y: f64, z: f64) -> Vec3 {
return Vec3 { x, y, z };
}
pub fn dot(&self, other: &Vec3) -> f64 {
return self.x * other.x + self.y * other.y + self.z * other.z;
}
}
impl Add for Vec3 {
type Output = Vec3;
fn add(self, other: Vec3) -> Vec3 {
return Vec3 {
x: self.x + other.x,
y: self.y + other.y,
z: self.z + other.z,
};
}
}
impl Sub for Vec3 {
type Output = Vec3;
fn sub(self, other: Vec3) -> Vec3 {
return Vec3 {
x: self.x - other.x,
y: self.y - other.y,
z: self.z - other.z,
};
}
}
impl Mul<f64> for Vec3 {
type Output = Vec3;
fn mul(self, scalar: f64) -> Vec3 {
return Vec3 {
x: self.x * scalar,
y: self.y * scalar,
z: self.z * scalar,
};
}
}
impl Neg for Vec3 {
type Output = Vec3;
fn neg(self) -> Vec3 {
return Vec3 {
x: -self.x,
y: -self.y,
z: -self.z,
};
}
}
impl Display for Vec3 {
fn display(&self) -> String {
return `Vec3({self.x}, {self.y}, {self.z})`;
}
}
fn run() with Stdout {
let v1 = Vec3::new(1.0, 2.0, 3.0);
let v2 = Vec3::new(4.0, 5.0, 6.0);
let sum = v1 + v2;
println(`v1 + v2 = {sum}`); // Vec3(5.0, 7.0, 9.0)
let diff = v1 - v2;
println(`v1 - v2 = {diff}`); // Vec3(-3.0, -3.0, -3.0)
let scaled = v1 * 2.0;
println(`v1 * 2.0 = {scaled}`); // Vec3(2.0, 4.0, 6.0)
let negated = -v1;
println(`-v1 = {negated}`); // Vec3(-1.0, -2.0, -3.0)
let dot = v1.dot(&v2);
println(`v1 · v2 = {dot}`); // 32.0
}
Custom Matrix with Index Operator
struct Matrix {
data: List<f64>,
rows: u32,
cols: u32,
}
impl Matrix {
pub fn new(rows: u32, cols: u32) -> Matrix {
let size = rows * cols;
let mut data: List<f64> = [];
for let mut i = 0; i < size; i += 1 {
data.push(0.0);
}
return Matrix { data, rows, cols };
}
}
impl Index<[u32, u32]> for Matrix {
type Output = f64;
fn index(&self, idx: [u32, u32]) -> &f64 {
let [row, col] = idx;
assert row < self.rows, "row index out of bounds";
assert col < self.cols, "col index out of bounds";
let offset = (row * self.cols + col) as usize;
return &self.data[offset];
}
}
impl IndexMut<[u32, u32]> for Matrix {
type Output = f64;
fn index_mut(&mut self, idx: [u32, u32]) -> &mut f64 {
let [row, col] = idx;
assert row < self.rows, "row index out of bounds";
assert col < self.cols, "col index out of bounds";
let offset = (row * self.cols + col) as usize;
return &mut self.data[offset];
}
}
fn run() {
let mut m = Matrix::new(3, 3);
// Write using index operator
m[[0, 0]] = 1.0;
m[[1, 1]] = 5.0;
m[[2, 2]] = 9.0;
// Read using index operator
let diagonal_sum = m[[0, 0]] + m[[1, 1]] + m[[2, 2]];
assert diagonal_sum == 15.0;
}
Generic Numeric Function
// Works with any numeric type that implements Add
fn sum_three<T: Add<Output = T>>(a: T, b: T, c: T) -> T {
return a + b + c;
}
fn run() {
let int_sum = sum_three(1, 2, 3); // 6 (i32)
let float_sum = sum_three(1.0, 2.0, 3.0); // 6.0 (f64)
let v1 = Vec3::new(1.0, 0.0, 0.0);
let v2 = Vec3::new(0.0, 1.0, 0.0);
let v3 = Vec3::new(0.0, 0.0, 1.0);
let vec_sum = sum_three(v1, v2, v3); // Vec3(1.0, 1.0, 1.0)
}
Implementation Notes
Compiler Desugaring
The compiler desugars operator expressions to trait method calls during the AST → TIR phase:
// Source
let result = a + b;
// Desugared to
let result = a.add(b);
For index operators:
// Source
let value = matrix[[row, col]];
matrix[[row, col]] = 42.0;
// Desugared to
let value = *matrix.index([row, col]);
*matrix.index_mut([row, col]) = 42.0;
Type Checking
The type checker verifies that:
- The trait is implemented for the operand type(s)
- The associated
Outputtype matches the expected type - Generic bounds are satisfied
Wasm Code Generation
Operator trait calls compile to:
- Monomorphization for generic types (static dispatch, no overhead)
- Direct calls for concrete types (inlined when possible)
- Vtable dispatch for trait objects (
&dyn Add)
Future Considerations
Callable Objects (Fn traits)
Future WEP will define Fn, FnMut, and FnOnce traits for callable objects (similar to Rust). This is separate from this WEP.
Deref Coercion
Rust's Deref trait enables smart pointers and automatic coercion. This may be added in a future WEP.
References
Language Designs
- Rust std::ops - Operator Overloading Traits
- Rust By Example - Operator Overloading
- Rust RFC 0953 - op-assign (Compound Assignment Operators)
- Rust std::ops::AddAssign
- C++ operator overloading
- Kotlin Operator Overloading
- Swift Operator Overloading
- Python Dunder Methods
- Zig Issue #8567 - MathType for operator overloading
- Why Zig When There is Already Rust? - No operator overloading
Related WEPs
- WEP: Struct and Trait System - Trait foundation
- WEP: Operator Precedence - Operator precedence rules
- WEP: Value Semantics - Value semantics and cloning
Sources
- Rust std::ops documentation
- Rust By Example - Operators and Overloading
- Rust RFC 0953 - Compound assignment operators
- Rust AddAssign trait
- C++ operator overloading reference
- Learn C++ - Overloading arithmetic operators using friend functions
- Kotlin operator overloading documentation
- Swift operator overloading guide
- Python dunder methods
- Zig operator overloading discussion
- Zig design philosophy
