Operator Traits Proposal
Status: Approved Approved: 2026-01-31 Author: Claude Created: 2026-01-31 Depends On: default-type-parameters-proposal.md, default-associated-types-proposal.md Enables: duration-size-to-stdlib.md
Summary
Define traits for arithmetic, bitwise, and unary operators that user-defined types can implement to support operator syntax. The compiler desugars operators to trait method calls.
Motivation
Currently, operators (+, -, *, /, %, -x, ~x, !x) are hardcoded in the compiler for built-in types only. User-defined types cannot use operator syntax:
type Vector2 = { x: float, y: float }
// Today: verbose method calls
let sum = v1.add(other: v2)
// Goal: natural operator syntax
let sum = v1 + v2
This limitation prevents:
- Mathematical types (vectors, matrices, complex numbers)
- Unit types (Duration, Size, Currency, Temperature)
- Wrapper types that should behave like their inner type
- Domain-specific numeric types
Design
Trait Definitions
Operator traits are defined in the prelude:
// Binary arithmetic operators
trait Add<Rhs = Self> {
type Output = Self
@add (self, rhs: Rhs) -> Self.Output
}
trait Sub<Rhs = Self> {
type Output = Self
@subtract (self, rhs: Rhs) -> Self.Output
}
trait Mul<Rhs = Self> {
type Output = Self
@multiply (self, rhs: Rhs) -> Self.Output
}
trait Div<Rhs = Self> {
type Output = Self
@divide (self, rhs: Rhs) -> Self.Output
}
// Note: Method is `divide` (not `div`) because `div` is a keyword (floor division operator)
trait FloorDiv<Rhs = Self> {
type Output = Self
@floor_divide (self, rhs: Rhs) -> Self.Output
}
trait Rem<Rhs = Self> {
type Output = Self
@remainder (self, rhs: Rhs) -> Self.Output
}
// Unary operators
trait Neg {
type Output = Self
@negate (self) -> Self.Output
}
trait Not {
type Output = Self
@not (self) -> Self.Output
}
trait BitNot {
type Output = Self
@bit_not (self) -> Self.Output
}
// Bitwise operators
trait BitAnd<Rhs = Self> {
type Output = Self
@bit_and (self, rhs: Rhs) -> Self.Output
}
trait BitOr<Rhs = Self> {
type Output = Self
@bit_or (self, rhs: Rhs) -> Self.Output
}
trait BitXor<Rhs = Self> {
type Output = Self
@bit_xor (self, rhs: Rhs) -> Self.Output
}
trait Shl<Rhs = int> {
type Output = Self
@shift_left (self, rhs: Rhs) -> Self.Output
}
trait Shr<Rhs = int> {
type Output = Self
@shift_right (self, rhs: Rhs) -> Self.Output
}
Operator Desugaring
The compiler desugars operators to trait method calls:
| Operator | Desugars To |
|---|---|
a + b | a.add(rhs: b) |
a - b | a.subtract(rhs: b) |
a * b | a.multiply(rhs: b) |
a / b | a.divide(rhs: b) |
a div b | a.floor_divide(rhs: b) |
a % b | a.remainder(rhs: b) |
-a | a.negate() |
!a | a.not() |
~a | a.bit_not() |
a & b | a.bit_and(rhs: b) |
a | b | a.bit_or(rhs: b) |
a ^ b | a.bit_xor(rhs: b) |
a << b | a.shift_left(rhs: b) |
a >> b | a.shift_right(rhs: b) |
Existing Comparison Operators
Comparison operators already use traits:
| Operator | Trait | Method |
|---|---|---|
a == b | Eq | a.equals(other: b) |
a != b | Eq | !a.equals(other: b) |
a < b | Comparable | a.compare(other: b).is_less() |
a <= b | Comparable | a.compare(other: b).is_less_or_equal() |
a > b | Comparable | a.compare(other: b).is_greater() |
a >= b | Comparable | a.compare(other: b).is_greater_or_equal() |
These remain unchanged.
Built-in Implementations
Primitives have built-in implementations:
impl int: Add {
type Output = int
@add (self, rhs: int) -> int = /* intrinsic */
}
impl float: Add {
type Output = float
@add (self, rhs: float) -> float = /* intrinsic */
}
impl str: Add {
type Output = str
@add (self, rhs: str) -> str = /* intrinsic: concatenation */
}
impl Duration: Add {
type Output = Duration
@add (self, rhs: Duration) -> Duration = /* intrinsic */
}
// ... etc for all primitives
Mixed-Type Operations
Traits support different right-hand-side types:
impl Duration: Mul<int> {
type Output = Duration
@multiply (self, n: int) -> Duration = Duration.from_nanoseconds(ns: self.nanoseconds() * n)
}
impl Duration: Div<int> {
type Output = Duration
@divide (self, n: int) -> Duration = Duration.from_nanoseconds(ns: self.nanoseconds() / n)
}
// Usage
let doubled = 5s * 2 // Duration * int -> Duration
let halved = 10s / 2 // Duration / int -> Duration
Commutative Mixed-Type Operations
For operations where both orderings should be valid (e.g., int * Duration and Duration * int), implement both directions explicitly:
// Duration * int
impl Duration: Mul<int> {
type Output = Duration
@multiply (self, n: int) -> Duration = Duration.from_nanoseconds(ns: self.nanoseconds() * n)
}
// int * Duration
impl int: Mul<Duration> {
type Output = Duration
@multiply (self, d: Duration) -> Duration = d * self // Delegate to Duration * int
}
// Usage
let a = 5s * 2 // Duration * int -> Duration
let b = 2 * 5s // int * Duration -> Duration (same result)
The compiler does not automatically commute operands. Each ordering requires an explicit implementation.
User-Defined Example
type Vector2 = { x: float, y: float }
impl Vector2: Add {
@add (self, rhs: Vector2) -> Self = Vector2 {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
impl Vector2: Sub {
@subtract (self, rhs: Vector2) -> Self = Vector2 {
x: self.x - rhs.x,
y: self.y - rhs.y,
}
}
impl Vector2: Mul<float> {
@multiply (self, scalar: float) -> Self = Vector2 {
x: self.x * scalar,
y: self.y * scalar,
}
}
impl Vector2: Neg {
@negate (self) -> Self = Vector2 { x: -self.x, y: -self.y }
}
// Usage
let a = Vector2 { x: 1.0, y: 2.0 }
let b = Vector2 { x: 3.0, y: 4.0 }
let sum = a + b // Vector2 { x: 4.0, y: 6.0 }
let diff = a - b // Vector2 { x: -2.0, y: -2.0 }
let scaled = a * 2.0 // Vector2 { x: 2.0, y: 4.0 }
let negated = -a // Vector2 { x: -1.0, y: -2.0 }
Chaining
Operators chain naturally with method calls:
let result = Vector2.zero()
.add(rhs: offset)
.mul(scalar: 2.0)
.normalize()
// Or with operators
let result = ((Vector2.zero() + offset) * 2.0).normalize()
Derivation
Common cases can use #derive:
// For newtypes wrapping numeric types
#derive(Add, Sub, Mul, Div)
type Meters = { value: float }
// Generates:
impl Meters: Add {
@add (self, rhs: Meters) -> Self = Meters { value: self.value + rhs.value }
}
// ... etc
Language Features Required
1. Default Type Parameters on Traits (REQUIRED)
The syntax trait Add<Rhs = Self> requires default type parameters:
trait Add<Rhs = Self> { // Rhs defaults to Self if not specified
type Output = Self
@add (self, rhs: Rhs) -> Self.Output
}
// These are equivalent:
impl Point: Add { ... }
impl Point: Add<Point> { ... }
Status: APPROVED — See default-type-parameters-proposal.md
2. Default Associated Types (REQUIRED)
The syntax type Output = Self requires default associated types:
trait Add<Rhs = Self> {
type Output = Self // Defaults to Self if not specified
@add (self, rhs: Rhs) -> Self.Output
}
// Can omit Output if it's Self:
impl Point: Add {
@add (self, rhs: Point) -> Self = ... // Output inferred as Self = Point
}
Status: APPROVED — See default-associated-types-proposal.md
3. Self in Associated Type Defaults (REQUIRED)
Self must be usable in associated type default values:
trait Add<Rhs = Self> {
type Output = Self // Self refers to implementing type
}
Status: IMPLEMENTED — Covered by default-type-parameters and default-associated-types proposals.
4. Derive Macros for Operator Traits (NICE TO HAVE)
#derive(Add, Sub, ...) for newtypes:
#derive(Add, Sub, Mul, Div)
type Celsius = { value: float }
Status: NOT IMPLEMENTED — derive system exists but not for operators. Defer to future proposal.
Implementation Plan
Phase 1: Language Prerequisites
- Implement default type parameters on traits ✅ (approved)
- Implement default associated types ✅ (approved)
- Verify
Selfworks in associated type defaults ✅ (covered by above)
Phase 2: Operator Traits
- Define operator traits in prelude (
Add,Sub,Mul,Div,FloorDiv,Rem,Neg,Not,BitNot, etc.) - Modify type checker to desugar operators to trait method calls
- Modify evaluator to dispatch operators via trait impls
- Add built-in impls for primitives (int, float, str, Duration, Size, etc.)
Phase 3: Testing
- User-defined types with operators
- Mixed-type operations (
Duration * int,int * Duration) - Chaining operators and methods
- Error messages for missing impls
Phase 4: Derive Support (Optional)
- Add
#derive(Add),#derive(Sub), etc. - Generate appropriate impls for newtypes
Error Messages
Good error messages are critical:
let x = Point { x: 1, y: 2 } + 5
// Error: cannot add `Point` and `int`
// --> file.ori:3:9
// |
// 3 | let x = Point { x: 1, y: 2 } + 5
// | ^^^^^^^^^^^^^^^^^^^^^^^^
// |
// = note: `Point` implements `Add<Point>` but not `Add<int>`
// = help: consider implementing `Add<int>` for `Point`: `impl Point: Add<int> { ... }`
Alternatives Considered
Method-Only Approach
Require explicit method calls instead of operators:
let sum = v1.add(other: v2)
Rejected: Too verbose for mathematical code, doesn’t match user expectations.
Operator Functions
Define operators as standalone functions:
@(+) (a: Vector2, b: Vector2) -> Vector2 = ...
Rejected: Doesn’t integrate with trait system, can’t have multiple impls.
Compiler Intrinsics Only
Keep operators for built-in types only.
Rejected: Prevents Duration/Size from moving to stdlib, limits user types.
References
- Rust:
std::opsmodule - Haskell: Numeric type classes
- Swift: Operator declarations
- Archived design:
docs/ori_lang/v2026/archived-design/appendices/C-builtin-traits.md