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.
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);
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);
}
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)
}
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)
}
fn *(x: int, y: int) -> int {
return 999; // Custom multiplication: always return 999
}
fn main() {
let result = 5 * 7;
print(result); // Prints: 999
}
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)
}
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)
}
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!
}
}
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!
}
}
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
}
}
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
}
}
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
}
}
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
}
}
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
}
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
}
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"); }
}
// 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
}
// Matrix multiplication
fn *(m1: Matrix, m2: Matrix) -> Matrix {
// Implementation of matrix multiplication
return multiplyMatrices(m1, m2);
}
fn main() {
let result = matrixA * matrixB; // Clean syntax
}
fn +(s1: string, s2: string) -> string {
return concat(s1, s2);
}
fn main() {
let greeting = "Hello, " + "World!";
print(greeting);
}
// Case-insensitive string comparison
fn ==(s1: string, s2: string) -> bool {
return toLowerCase(s1) == toLowerCase(s2);
}
fn main() {
if "Hello" == "hello" {
print("Equal (case-insensitive)");
}
}
// 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");
}
}
// 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;
}
// 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);
}
// ✅ 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!
}
// ✅ 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
}
// ✅ 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!
}
// 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
};
}
// ✅ 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?
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);
}
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
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: