etch

Operator Overloading

Etch allows you to define custom behavior for operators by defining functions with operator symbols as names. This enables you to create intuitive syntax for custom types and modify how operators work for built-in types.

Table of Contents

  1. Overview
  2. Arithmetic Operators
  3. Comparison Operators
  4. Operator Functions
  5. Use Cases
  6. Best Practices

Overview

In Etch, operators are just functions with special names. When you write a + b, the compiler looks for a function named + that takes two parameters matching the types of a and b.

Key Principle: Operators are syntactic sugar for function calls.

// These are equivalent:
let sum = a + b;
let sum = +(a, b);

Defining Operator Functions

Define a function with an operator symbol as its name:

// Define custom addition
fn +(a: int, b: int) -> int {
    return a + b + 1;  // Custom: add 1 to result
}

fn main() {
    let x = 5 + 3;  // Calls custom +: returns 9 instead of 8
    print(x);
}

Arithmetic Operators

Addition (+)

fn +(a: int, b: int) -> int {
    return a * 10;  // Custom addition: multiply first arg by 10
}

fn main() {
    let x = 5;
    let y = 3;
    print(x + y);  // Prints: 50 (5 * 10)
}

Subtraction (-)

fn -(a: int, b: int) -> int {
    return a / 2;  // Custom subtraction: divide first arg by 2
}

fn main() {
    let x = 10;
    let y = 3;
    print(x - y);  // Prints: 5 (10 / 2)
}

Multiplication (*)

fn *(x: int, y: int) -> int {
    return 999;  // Custom multiplication: always return 999
}

fn main() {
    let result = 5 * 7;
    print(result);  // Prints: 999
}

Division (/)

fn /(a: int, b: int) -> int {
    return a % 3;  // Custom division: return remainder when dividing by 3
}

fn main() {
    let x = 10;
    let y = 7;
    print(x / y);  // Prints: 1 (10 % 3)
}

Modulo (%)

fn %(x: int, y: int) -> int {
    return x + y + 100;  // Custom modulo: add both args plus 100
}

fn main() {
    let result = 5 % 3;
    print(result);  // Prints: 108 (5 + 3 + 100)
}

Comparison Operators

Equality (==)

fn ==(a: int, b: int) -> bool {
    return false;  // Custom equality: always return false
}

fn main() {
    let x = 5;
    let y = 5;

    if x == y {
        print("Equal");
    } else {
        print("Not equal");  // This prints!
    }
}

Inequality (!=)

fn !=(x: int, y: int) -> bool {
    return true;  // Custom inequality: always return true
}

fn main() {
    let x = 5;
    let y = 5;

    if x != y {
        print("Different");  // This prints!
    }
}

Less Than (<)

fn <(a: int, b: int) -> bool {
    return a > b;  // Custom: reverse the comparison
}

fn main() {
    let x = 5;
    let y = 3;

    if x < y {
        print("x is less");  // This prints because 5 > 3 is true
    }
}

Less Than or Equal (<=)

fn <=(x: int, y: int) -> bool {
    return x >= y;  // Custom: reverse the comparison
}

fn main() {
    let x = 5;
    let y = 3;

    if x <= y {
        print("x is less or equal");  // Prints because 5 >= 3 is true
    }
}

Greater Than (>)

fn >(a: int, b: int) -> bool {
    return a < b;  // Custom: reverse the comparison
}

fn main() {
    let x = 5;
    let y = 3;

    if x > y {
        print("x is greater");  // Doesn't print because 5 < 3 is false
    }
}

Greater Than or Equal (>=)

fn >=(x: int, y: int) -> bool {
    return x <= y;  // Custom: reverse the comparison
}

fn main() {
    let x = 5;
    let y = 3;

    if x >= y {
        print("x is greater or equal");  // Doesn't print because 5 <= 3 is false
    }
}

Compound Assignment Operators (+=, -=, *=, /=, %=)

Etch now understands the common compound assignment operators. These are syntactic sugar for “read, apply operator, store” sequences and work for both integers and floats.

fn main() {
    var score: int = 10;
    score += 5;  // score = score + 5
    score -= 2;  // score = score - 2
    score *= 4;
    score /= 2;
    score %= 3;

    print(score);  // Prints: 1
}

The parser also detects the long form a = a op b and automatically rewrites it to the corresponding compound operator, keeping the generated bytecode identical whether you write count = count + 1 or count += 1.

Compound assignments now apply to indexed and field targets as well. The compiler keeps the array/object lookup stable, emits fused bytecode when possible, and enforces the same type rules as the long form.

type Point = object {
    x: int;
    y: int;
};

fn main() {
    var values: array[int] = [10, 5, 2];
    var point = Point(x: 3, y: 4);

    values[1] += 3;
    values[0] -= 2;
    point.x *= 2;
    point.y %= 3;

    print(values[1]); // 8
    print(point.x);   // 6
}

Operator Functions

All Arithmetic Operators Together

fn +(a: int, b: int) -> int { return a + b; }
fn -(a: int, b: int) -> int { return a - b; }
fn *(a: int, b: int) -> int { return a * b; }
fn /(a: int, b: int) -> int { return a / b; }
fn %(a: int, b: int) -> int { return a % b; }

fn main() {
    let x = 10;
    let y = 3;

    print(x + y);  // 13
    print(x - y);  // 7
    print(x * y);  // 30
    print(x / y);  // 3
    print(x % y);  // 1
}

All Comparison Operators Together

fn ==(a: int, b: int) -> bool { return a == b; }
fn !=(a: int, b: int) -> bool { return a != b; }
fn <(a: int, b: int) -> bool { return a < b; }
fn <=(a: int, b: int) -> bool { return a <= b; }
fn >(a: int, b: int) -> bool { return a > b; }
fn >=(a: int, b: int) -> bool { return a >= b; }

fn main() {
    let x = 5;
    let y = 3;

    if x == y { print("equal"); }
    if x != y { print("not equal"); }
    if x < y { print("less than"); }
    if x <= y { print("less or equal"); }
    if x > y { print("greater than"); }
    if x >= y { print("greater or equal"); }
}

Use Cases

1. Vector Addition

// Assume we have a Vector type (when structs are added)
fn +(v1: Vector, v2: Vector) -> Vector {
    return Vector {
        x: v1.x + v2.x,
        y: v1.y + v2.y,
        z: v1.z + v2.z
    };
}

fn main() {
    let v1 = Vector { x: 1, y: 2, z: 3 };
    let v2 = Vector { x: 4, y: 5, z: 6 };
    let v3 = v1 + v2;  // Natural vector addition syntax
}

2. Matrix Operations

// Matrix multiplication
fn *(m1: Matrix, m2: Matrix) -> Matrix {
    // Implementation of matrix multiplication
    return multiplyMatrices(m1, m2);
}

fn main() {
    let result = matrixA * matrixB;  // Clean syntax
}

3. String Concatenation (if not built-in)

fn +(s1: string, s2: string) -> string {
    return concat(s1, s2);
}

fn main() {
    let greeting = "Hello, " + "World!";
    print(greeting);
}

4. Custom Equality

// Case-insensitive string comparison
fn ==(s1: string, s2: string) -> bool {
    return toLowerCase(s1) == toLowerCase(s2);
}

fn main() {
    if "Hello" == "hello" {
        print("Equal (case-insensitive)");
    }
}

5. Range Checking

// Define custom < for range checking
fn <(value: int, max: int) -> bool {
    return value >= 0 and value < max;
}

fn main() {
    let index = 5;
    let size = 10;

    if index < size {
        print("Valid index");
    }
}

6. Saturating Arithmetic

// Addition that saturates instead of overflowing
fn +(a: int, b: int) -> int {
    if a > 0 and b > IMax - a {
        return IMax;  // Saturate at maximum
    }
    if a < 0 and b < IMin - a {
        return IMin;  // Saturate at minimum
    }
    return a + b;
}

7. Modular Arithmetic

// All operations modulo a prime
let MODULO = 1000000007;

fn +(a: int, b: int) -> int {
    return (a + b) % MODULO;
}

fn *(a: int, b: int) -> int {
    return (a * b) % MODULO;
}

fn main() {
    let x = 999999999;
    let y = 100;
    let sum = x + y;  // Automatically wrapped
    print(sum);
}

Best Practices

1. Maintain Expected Behavior

// ✅ Good: Operators behave intuitively
fn +(v1: Vector, v2: Vector) -> Vector {
    return addVectors(v1, v2);  // Makes sense for vectors
}

// ❌ Bad: Surprising behavior
fn +(a: int, b: int) -> int {
    return a * b;  // Confusing: + doesn't add!
}

2. Keep Mathematical Properties

// ✅ Good: Maintains commutativity where expected
fn +(a: int, b: int) -> int {
    return a + b;  // a + b == b + a
}

// ⚠️ Careful: Breaking commutativity
fn +(a: string, b: string) -> string {
    return a + " " + b;  // a + b != b + a
}

3. Match Type System

// ✅ Good: Operator signature matches usage
fn +(a: Vector, b: Vector) -> Vector {
    return addVectors(a, b);
}

// ❌ Bad: Unexpected return type
fn +(a: int, b: int) -> string {
    return "sum";  // Confusing!
}

4. Document Custom Operators

// Addition for vectors: component-wise sum
// Returns: Vector with (v1.x + v2.x, v1.y + v2.y, v1.z + v2.z)
fn +(v1: Vector, v2: Vector) -> Vector {
    return Vector {
        x: v1.x + v2.x,
        y: v1.y + v2.y,
        z: v1.z + v2.z
    };
}

5. Don’t Overuse

// ✅ Good: Operators for natural operations
fn +(v1: Vector, v2: Vector) -> Vector { ... }
fn *(scalar: int, v: Vector) -> Vector { ... }

// ❌ Bad: Operators for unrelated operations
fn +(user: User, database: Database) -> bool { ... }  // What does this mean?

6. Preserve Operator Precedence

Etch uses standard operator precedence:

Your custom operators follow the same precedence rules:

fn +(a: int, b: int) -> int { return a + b; }
fn *(a: int, b: int) -> int { return a * b; }

fn main() {
    let result = 2 + 3 * 4;  // Still evaluates as 2 + (3 * 4) = 14
    print(result);
}

7. Consider UFCS Alternative

Sometimes UFCS is clearer than operator overloading:

// Using operators
let result = vec1 + vec2 * scalar;

// Using UFCS
let result = vec1.add(vec2.multiply(scalar));

// Choose based on clarity

Limitations

  1. Can’t create new operators: Only existing operators can be overloaded
  2. Can’t change precedence: Operator precedence is fixed
  3. Can’t change associativity: Left/right associativity is fixed
  4. Must match operator type: Binary operators need two parameters

Summary

Operator overloading in Etch provides:

Natural syntax - Make custom types work with familiar operators ✅ Type safety - Operators are just typed functions ✅ Flexibility - Define behavior per type ✅ Clarity - When used well, improves readability

Use operator overloading to make your code more expressive, but don’t sacrifice clarity for brevity.


See Also: