docs / guide / The Cajeta Guide

Chapters — 16. Operator overloading
  1. Introduction
  2. 0. Introduction
  3. 1. Installation
  4. 2. Kick the tires
  5. 3. Your first project
  6. 4. Running your application
  7. 5. Debugging
  8. 6. Keywords
  9. 7. Comments
  10. 8. Native types
  11. 9. Type kinds
  12. 10. Allocation
  13. 11. Ownership & borrowing
  14. 12. Control flow
  15. 13. Strings & formatting
  16. 14. Templates & wildcards
  17. 15. Lambdas & captures
  18. 16. Operator overloading
  19. 17. Inheritance
  20. 18. Annotations & synthesis
  21. 19. DI & aspects
  22. 20. Error handling
  23. 21. Reflection
  24. 22. Differentiation
  25. 23. Code coverage
  26. 23. Profiling
  27. 24. Notebooks
  28. 25. Writing XPU Kernels

16 — Operator overloading

Two rules carry the whole design. Binary operators and non-mutating unaries are public static: both operands are explicit parameters, nothing mutates, and the result is a fresh value. Mutating unaries (++, --) and indexed access ([], []=) are instance methods: the receiver is the target, and the call site needs a mutable borrow. Tour demo: OperatorOverloadDemo; spec: OperatorOverloading.md.

Overloadable today: arithmetic + - * / %, bitwise & | ^ << >>, comparisons == != < > <= >=, unary - and +, instance ++ --, and [] / []=. Boolean && / || are deliberately not overloadable — a function call can’t preserve short-circuit evaluation. Unary ! and ~ are specified but do not compile yet (roadmap).

String defines == by content and < > <= >= by compareTo, bytewise UTF-8 order with null first. So ordered code written with <, such as Sort.sort<T> or RedBlackTree<K, V>, takes String keys as it takes primitives.

Static operators

@AutoHash
public class Vec2 {
    public int32 x;
    public int32 y;
    public Vec2(int32 x, int32 y) { this.x = x; this.y = y; }

    public static #Vec2 operator+ (Vec2 a, Vec2 b) {
        return heap Vec2(a.x + b.x, a.y + b.y);
    }
    public static #Vec2 operator- (Vec2 a, Vec2 b) {
        return heap Vec2(a.x - b.x, a.y - b.y);
    }
    public static #Vec2 operator- (Vec2 v) {
        return heap Vec2(0 - v.x, 0 - v.y);
    }
    public static boolean operator== (Vec2 a, Vec2 b) {
        return a.x == b.x && a.y == b.y;
    }
}
Vec2 a = stack Vec2(1, 2);
Vec2 b = stack Vec2(3, 4);
Vec2 sum = a + b;
Vec2 neg = -a;
boolean same = a == b;

a + b lowers to Vec2.operator+(a, b); no operand is mutated. Unary and binary - are distinguished by arity. Dispatch is on the static type of the LHS only, and it is not virtual — so v * 2 needs an overload on Vec2, and the reversed 2 * v would need one on int32, which primitives can’t declare. One direction is the practical scope.

Derived operators

Declare == and the compiler derives != as its negation. Declare < and <=, >, >= come free. Compound assignment derives from the binary form: a += b rewrites to a = Vec2.operator+(a, b) with no operator+= declared. When an explicit instance operator+= is declared, it wins — useful when in-place mutation beats allocate-and-reassign:

public class Acc {
    public int32 total;
    public Acc() { this.total = 0; }
    public void operator+= (int32 n) { this.total = this.total + n; }
}

The == / hash() pairing rule

A class that defines structural operator== must also make hash() structural, or HashMap<K, V> mis-keys: two ==-equal instances with identity hashes land in different buckets. The compiler enforces the pairing with CAJETA_WARN_HASH_EQUALS_MISMATCH when operator== appears without a hash() override. @AutoHash (on Vec2 above) synthesizes a structural hash() from the fields, keeping it consistent with == automatically.

Instance operators

++ and -- mutate the receiver: no parameters, void return. Both t++ and ++t lower to t.operator++(). Subscripts read and write through a pair of instance methods — g[i] lowers to g.operator[](i), and g[i] = v to g.operator[]=(i, v). Subscript expressions hand the index over as int64.

public class Ticker {
    public int32 count;
    public Ticker() { this.count = 0; }
    public void operator++ () { this.count = this.count + 1; }
    public void operator-- () { this.count = this.count - 1; }
}

public class Grid {
    public int32[] cells;
    public Grid() { this.cells = heap int32[4]; }
    public int32 operator[] (int64 i) { return this.cells[(int32) i]; }
    public void operator[]= (int64 i, int32 v) { this.cells[(int32) i] = v; }
}
Ticker t = stack Ticker();
t++;
t++;
t--;

Grid g = stack Grid();
int64 i = 1;
g[i] = 20;
int32 read = g[i];

The stdlib’s HashMap exposes the same subscript pair — m[k] is this mechanism, not special-cased syntax.

Next: 17 — Inheritance.

Source: docs/guide/16-operators.md · 2 min · 418 words