Docs menu

Generics and Operator Overloading

Generics let you write a function or struct once and use it for many types. Operator overloading lets +, == and [] work on your own types.

By the end of this page you will

  • Write generic functions and structs.
  • Explain what “monomorphized” means, and why nothing is boxed.
  • Overload operators for your own types, including indexing.

In plain words

A generic is a cookie cutter that does not care what the dough is. List<T> is a list of something; write List<int> and you get a list of integers, List<string> and you get a list of text. Turf makes a separate, fully specialised copy for each type you actually use, so a List<int> stores real ints.

Operator overloading means telling Turf what +, == or [] should do for a type you made. The standard collections use both: list[i] is a generic operator.

This page covers two features that let you write code once and use it for many types: generics (functions and structs parameterized by type) and operator overloading (giving +, ==, [] and friends a meaning on your own types). The standard collections use both: List<T> is a generic struct, and list[i] is a generic operator.

Generics

Generics are monomorphized. A generic function, method or struct is a template; each distinct combination of type arguments you use produces its own fully specialized copy, checked and compiled for exactly those types. List<int> and List<string> are two different structs with two different sets of compiled methods, and nothing is boxed or erased: a List<int> stores real ints, a Box<Big> holds a real Big by value, whatever its size.

What follows from that:

  • sizeof(T) inside a generic body is the real size of the type argument.
  • A generic struct instantiated with a large struct works the same as one instantiated with int. There is no need to store pointers to large elements (List<Entry*>) and no reason to; use List<Entry>.
  • A generic body is type-checked again for every instantiation, so an error that depends on the type argument is reported for that instantiation (see Errors inside a generic body).
  • Instantiations that no code asks for are never compiled.
generics_and_operators_generics.tr
import list::List;import test;struct Entry {    key: string,    value: string,    hits: int}fn main(): int {    mut store: List<Entry> = List::default();    mut e: Entry = Entry::default();    e.key = "language";    e.value = "Turf";    store.push(e);    test::assert(store[0].key == "language", "elements are stored by value");    test::assert(store.get(0).value == "Turf", "whatever their size");    return 0;}

Generic functions

Type parameters are declared in < > right after fn. They can be used anywhere a type can: parameter types, the return type, local variables, array types, other generic types and function types.

generics_and_operators_generic_functions.tr
import test;fn<T> identity(x: T): T {    return x;}fn<T> first(items: T[]): T {    return items[0];}fn<T> size_of_value(x: T): int {    return sizeof(T);}fn<T> count_down(x: T, n: int): int {    if (n == 0) {        return 0;    }    return 1 + count_down(x, n - 1);}fn main(): int {    test::assert(identity(42) == 42, "identity<int>");    test::assert(identity("hello") == "hello", "identity<string>");    test::assert(identity(3.5) == 3.5, "identity<double>");    mut arr: int[3] = [5, 6, 7];    test::assert(first(arr) == 5, "T[] binds T to the element type");    test::assert(size_of_value(1) == 8, "sizeof(T) is the real size");    test::assert(size_of_value(true) == 1, "...for every instantiation");    test::assert(count_down("s", 3) == 3, "a generic may call itself");    return 0;}

A generic function may take a function-typed parameter that mentions its own type parameters. This is how a generic body uses an operation the caller chooses rather than one T must provide, and how the standard library’s List.sort(cmp: fn(T, T): int) is written:

generics_and_operators_generic_functions_2.tr
import test;fn<T> largest(a: T, b: T, greater: fn(T, T): bool): T {    if (greater(a, b)) {        return a;    }    return b;}fn greater_int(a: int, b: int): bool {    return a > b;}fn greater_string(a: string, b: string): bool {    return a > b;}fn<T> apply_twice(x: T, f: fn(T): T): T {    return f(f(x));}fn increment(n: int): int {    return n + 1;}fn main(): int {    test::assert(largest(3, 9, greater_int) == 9, "T = int");    test::assert(largest("pear", "apple", greater_string) == "pear", "T = string");    test::assert(apply_twice(1, increment) == 3, "fn(T): T");    return 0;}

Several generic functions may share a name if their parameter lists differ, exactly like ordinary overloads.

How type arguments are chosen

Calls to a generic function infer the type arguments from the arguments. identity(42) binds T to int because the argument is an int; first(arr) binds T to int because arr is int[3]. Every type parameter must appear in the parameter list, since that is the only place it can be inferred from (fn<T> make(): T is E129 at the declaration).

Every argument that mentions a type parameter must agree on it. Inference never widens: with fn<T> pair(a: T, b: T): T, pair(1, 2.5) and pair(1, "a") are E056, because T would be int from the first argument and something else from the second. Convert one argument (pair(1 to double, 2.5)).

A generic function used as a value takes them from the function type it is stored into. immut f: fn(int): int = identity; instantiates identity at int; so does passing identity to a parameter of type fn(int): int, or returning it from a function that returns one. With no function type to read (mut g = identity;) it is E137. See First-class functions.

There is no syntax for writing type arguments at a use. < after a name in an expression is always the less-than operator, so identity<int>(3) and Box<int>::default() do not parse (E110). Where a type argument cannot be inferred, put the type on a declaration instead:

generics_and_operators_e110.tr
fn<T> identity(x: T): T {    return x;}fn main(): int {    immut r: int = identity<int>(3);    return 0;}
Terminal
$ turf check generics_and_operators_e110.tr
generics_and_operators_e110.tr (1 error)

── error at line 6, col 29 [E110 · syntax error]

  Expected 'mutable' or 'immutable' to begin a declaration.
  Hint: if this is a generic type argument in an expression (`List<int>::default()`), put the type on the variable instead: `mut l: List<int> = List::default();`.

  6 │     immut r: int = identity<int>(3);
    │                             ^~~ Here!

  Run `turf --explain E110` for more details.

A generic struct takes its type arguments from the type it is stored into. Box::default() and Box::new(...) have no type arguments of their own; the declared type of the variable, parameter, field, return type or element they are assigned to supplies them:

generics_and_operators_how_type_arguments.tr
import list::List;import test;struct Box<T> {    val: T}fn<T> Box.new(v: T): void {    self.val = v;}fn<T> Box.get(): T {    return self.val;}fn make_box(): Box<string> {    return Box::new("made");}fn take(b: Box<int>, extra: int): int {    return b.get() + extra;}fn main(): int {    mut a: Box<int> = Box::new(42);    mut b: Box<int> = Box::default();    mut boxes: List<Box<int>> = List::default();    boxes.push(Box::new(7));    test::assert(a.get() == 42, "annotated variable");    test::assert(b.get() == 0, "default() zero-fills");    test::assert(make_box().get() == "made", "return type");    test::assert(take(Box::new(1), 2) == 3, "argument");    test::assert(boxes[0].get() == 7, "collection element");    return 0;}

The arguments then convert to those types the way any call’s arguments do, and only by an implicit widening: with Box<double> declared, Box::new(1) stores 1.0, and with Box<int32> declared an untyped literal such as Box::new(5) fits. Nothing narrows or changes kind: with Box<int> declared, Box::new(2.5) is E056, and so is Box::new(42) for a Box<string>. Write the conversion (Box::new(2.5 to int)).

When nothing supplies the type, there is nothing to infer from. mut l = List::default(); is rejected with E129 (Nothing to infer! Cannot infer the type arguments of a generic type):

generics_and_operators_e129.tr
import list::List;fn main(): int {    mut l = List::default();    return 0;}
Terminal
$ turf check generics_and_operators_e129.tr
generics_and_operators_e129.tr (1 error)

── error at line 4, col 5 [E129 · declaration]

  Nothing to infer!
  Cannot infer the type arguments of a generic type.

  Hint: 'guess' has nothing to infer the type argument from here -- annotate it explicitly instead, e.g. 'mut l: List<int> = ...;'.

  4 │     mut l = List::default();
    │     ^~~ Here!

  Run `turf --explain E129` for more details.

Write mut l: List<int> = List::default(); instead.

Generic types nest freely, with or without a space between the closing >s:

generics_and_operators_how_type_arguments_2.tr
import list::List;import map::Map;import test;fn main(): int {    mut grid: List<List<int>> = List::default();    mut row: List<int> = List::default();    grid.push(row);    grid[0].push(4);    test::assert(grid[0][0] == 4, "List<List<int>>");    mut index: Map<string, List<int>> = Map::default();    mut ids: List<int> = List::default();    index["a"] = ids;    index["a"].push(1);    test::assert(index["a"][0] == 1, "Map<K, List<V>>");    return 0;}

Generic structs and their methods

A struct declares its type parameters in < > after its name; several are comma-separated. Its fields may use them anywhere, including inside other generic types.

A method on a generic struct repeats the struct’s type parameters after fn: fn<T> Box.get() for struct Box<T>, fn<K, V> Map.put(...) for struct Map<K, V>. Leaving them off is E138 (Missing type parameters!), because self’s layout depends on the type arguments:

generics_and_operators_e138.tr
struct Box<T> {    val: T}fn Box.broken(): int {    return 1;}fn main(): int {    return 0;}
Terminal
$ turf check generics_and_operators_e138.tr
generics_and_operators_e138.tr (1 error)

── error at line 5, col 8 [E138 · declaration]

  Missing type parameters!
  'Box' is generic, so its method 'broken' must declare its type parameters too.

  Hint: Declare it as 'fn<T> Box.broken(...)' -- the same parameters as 'struct Box<T>'.

  5 │ fn Box.broken(): int {
    │        ^~~~~~ Here!

  Run `turf --explain E138` for more details.

A method can declare further type parameters of its own alongside the struct’s, and a method on a non-generic struct can be generic too. Each is inferred from the method’s arguments:

generics_and_operators_generic_structs_and.tr
import test;struct Pair<A, B> {    first: A,    second: B}fn<A, B> Pair.set_first(a: A): void {    self.first = a;}fn<A, B> Pair.swapped(): Pair<B, A> {    mut r: Pair<B, A> = Pair::default();    r.first = self.second;    r.second = self.first;    return r;}fn<A, B, U> Pair.pick(other: U): U {    return other;}struct Printer {    prefix: string}fn<T> Printer.echo(x: T): T {    return x;}fn main(): int {    mut p: Pair<int, string> = Pair::default();    p.set_first(7);    p.second = "x";    mut q: Pair<string, int> = p.swapped();    test::assert(q.first == "x" && q.second == 7, "Pair<string, int>");    test::assert(p.pick(9) == 9 && p.pick("s") == "s", "a method's own U");    mut pr: Printer = Printer::default();    test::assert(pr.echo(5) == 5 && pr.echo("hi") == "hi", "generic method, plain struct");    return 0;}

Structs are values: assigning a Box<int> to another variable copies it.

What a generic body can do with T

There are no traits or constraints. A generic body is checked against whatever T actually is, once per instantiation, so it can use any operation that type supports:

  • Operators: == and !=, ordering (< <= > >=), arithmetic (+ - * / % **), bitwise (& | ^ << >> ~), unary - and !, ++/-- and compound assignment. Each works when T has it, built in (numbers, bool, char, string by content, enums, pointers) or declared (operator < on a struct).
  • Fields and methods: x.name and x.method() when T is a struct that has them.
  • sizeof(T) and to casts involving T.
  • Calls to functions with T values as arguments. Overload resolution runs per instantiation, so hash(x) inside a generic body picks the string overload when T is string and the general one otherwise.
  • Generic structs and functions built from T (Box<T>, List<T>, helper<T>(...)).
generics_and_operators_what_a_generic.tr
import test;struct Money {    cents: int}operator <(a: Money, b: Money): bool { return a.cents < b.cents; }fn<T> largest(a: T, b: T): T {    if (a < b) {        return b;    }    return a;}fn<T> sum(a: T, b: T): T {    return a + b;}fn<T> describe(x: T): string {    return "value";}fn describe(x: string): string {    return "string";}fn<T> via_generic(x: T): string {    return describe(x);}fn main(): int {    test::assert(largest(3, 9) == 9, "T = int");    test::assert(largest("pear", "apple") == "pear", "T = string");    mut a: Money = Money::default();    a.cents = 250;    mut b: Money = Money::default();    b.cents = 199;    test::assert(largest(a, b).cents == 250, "T = Money, through its operator <");    test::assert(sum(1.5, 2.0) == 3.5, "T = double");    test::assert(via_generic(1) == "value", "T = int uses the generic fallback");    test::assert(via_generic("s") == "string", "T = string picks the string overload");    return 0;}

The template itself is never rejected for what T might lack: whether a < b works depends on what T is, so it is decided for each instantiation, and an instantiation whose T lacks the operation is an error at the call that asked for it, with the code the same mistake has anywhere else (E124 for a comparison, E057 for arithmetic, E068 for a field, E131 for a method). The next section shows how it reads. To accept types that lack an operation, take it as a function parameter, as the greater version of largest does above.

generics_and_operators_e124.tr
struct Point {    x: int}fn<T> largest(a: T, b: T): T {    if (a < b) {        return b;    }    return a;}fn main(): int {    mut p: Point = Point::default();    mut q: Point = Point::default();    return largest(p, q).x;     // E124: Point has no `<`}
Terminal
$ turf check generics_and_operators_e124.tr
generics_and_operators_e124.tr (1 error)

── error at line 15, col 19 [E124 · type mismatch]

  Oops! Something doesn't add up:
  In this instantiation: Cannot compare 'Point' with 'Point' using '<'.
  Note: the error itself, inside 'largest<Point>' (line 6, col 11)
  Note: in generic declaration 'largest' (line 5, col 7)

  15 │     return largest(p, q).x;     // E124: Point has no `<`
     │                   ^ Here!

  Run `turf --explain E124` for more details.

Errors inside a generic body

An operation that exists for some type arguments and not others is reported when the offending instantiation is requested. The error is anchored at the line in your code that asked for the instantiation. Notes under it give the chain of instantiations that led to the failing one and the generic declaration it came from. The error’s code is the one the same mistake has outside a generic.

Here Set<T> needs == on its elements, and a plain struct has none:

generics_and_operators_e124_2.tr
import set::Set;struct Big {    a: int,    b: int,    c: int}fn main(): int {    mut s: Set<Big> = Set::default();    mut b: Big = Big::default();    s.add(b);    return 0;}
Terminal
$ turf check generics_and_operators_e124_2.tr
generics_and_operators_e124_2.tr (1 error)

── error at line 12, col 6 [E124 · type mismatch]

  Oops! Something doesn't add up:
  In this instantiation: Cannot compare struct values with '=='. (Structs have no built-in comparison: compare the fields you care about (e.g. `a.x == b.x`), or declare `operator ==` for the type.)
  Note: 'Set<Big>.add' requires 'Set<Big>.contains' here (lib/std/collections/set.tr, line 156, col 13)
  Note: 'Set<Big>.contains' requires 'Set<Big>.index_of' here (lib/std/collections/set.tr, line 152, col 16)
  Note: 'Set<Big>.index_of' requires 'Set<Big>.bucket_find' here (lib/std/collections/set.tr, line 144, col 31)
  Note: the error itself, inside 'Set<Big>.bucket_find' (lib/std/collections/set.tr, line 70, col 40)
  Note: in generic declaration 'set::Set.bucket_find' (lib/std/collections/set.tr, line 59, col 11)

  12 │     s.add(b);
     │      ^ Here!

  Run `turf --explain E124` for more details.

Read the notes from top to bottom: add needed contains, which needed index_of, which needed bucket_find, and bucket_find is where == was applied to two Bigs. The fix here is to declare operator ==(a: Big, b: Big): bool (see Operator overloading).

The same reporting applies to a call that finds no overload for the concrete type (No overload of 'len' matches argument types (int).) or an operation such as indexing that the type does not support.

Three further limits protect the compiler from a generic that never finishes:

  • E079, generic instantiation depth exceeded. A generic that asks for a strictly bigger instantiation of itself every time it runs (wrap<int> calls wrap<&int> calls wrap<&&int> …) is stopped at a chain depth of 64. The error prints the chain. Keep the type fixed across the recursion and let only the value recurse, for example by passing a pointer or reference.
  • E079, too many generic instantiations. A generic that requests two or more new instantiations per level (f<T> calls f<Box<T>> and f<Pair<T>>) hits a cap on the total number of instantiations in the program.
  • E069, a struct that contains itself by value through a generic instantiation: with struct A<T> { val: T } and struct B { back: A<B> }, the instantiation A<B> holds a B that holds an A<B>. Use a reference or pointer for the field.
generics_and_operators_e079.tr
fn<T> wrap(x: &T): int {    return wrap(&x);}fn main(): int {    mut v: int = 1;    return wrap(&v);}
Terminal
$ turf check generics_and_operators_e079.tr
generics_and_operators_e079.tr (1 error)

── error at line 7, col 16 [E079 · declaration]

  generic instantiation depth exceeded (65 deep).
  chain: wrap<int> -> wrap<&int> -> wrap<&&int> -> wrap<&&&int> -> wrap<&&&&int> -> ... 56 more ... -> wrap<&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&... -> wrap<&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&... -> wrap<&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&... -> wrap<&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&...

  Hint: A generic function or struct that requests a new instantiation of itself every time it runs never bottoms out. If this is intentional (e.g. a type-level Peano count), it's past what this compiler will monomorphize -- restructure it to stop growing the type at some concrete depth.

  7 │     return wrap(&v);
    │                ^ Here!

  Run `turf --explain E079` for more details.

Visibility of generics

A generic function or struct follows the ordinary visibility rules: it is usable from another module only when declared pub. The body of a pub generic can call its own module’s private helpers; each instantiation is resolved in the module that declares the template, not in the module that requested it. Generic struct fields are private unless declared pub.

Operator overloading

An operator declaration gives a symbol a meaning on your own types. The call site is the plain operator: a + b, -a, list[i], m[k] = v.

Declaration syntax

An operator is a top-level declaration: the keyword operator, the symbol, a parameter list and a body. It is not .- or ::-qualified and is never static. The operand names are ordinary parameter names; self is a reserved word and cannot be one (E108).

generics_and_operators_declaration_syntax.tr
import test;struct Vec2 {    x: int,    y: int}operator +(a: Vec2, b: Vec2): Vec2 {    mut r: Vec2 = Vec2::default();    r.x = a.x + b.x;    r.y = a.y + b.y;    return r;}fn main(): int {    mut a: Vec2 = Vec2::default();    a.x = 1;    a.y = 2;    mut b: Vec2 = Vec2::default();    b.x = 10;    b.y = 20;    mut c: Vec2 = a + b;    test::assert(c.x == 11, "x components add");    test::assert(c.y == 22, "y components add");    return 0;}

The symbol follows operator directly. A type-parameter list, if any, goes between the keyword and the symbol (see Generic operators). A declaration with the wrong number of parameters is E115, and a declaration without a body is E107.

The overloadable operators

binary    +  -  *  /  %  **  &  |  ^  <<  >>  ==  !=  <  >  <=  >=
unary     -  !  ~
indexing  []   (read, x[i])        []=   (write, x[i] = v)

Binary operators take two parameters, unary operators one. - has both forms and the number of parameters tells them apart. [] takes (container, index) and []= takes (container, index, value).

generics_and_operators_the_overloadable_operators.tr
import test;struct Num {    v: int}fn num(v: int): Num {    mut n: Num = Num::default();    n.v = v;    return n;}operator +(a: Num, b: Num): Num { return num(a.v + b.v); }operator -(a: Num, b: Num): Num { return num(a.v - b.v); }operator -(a: Num): Num { return num(-a.v); }operator *(a: Num, b: Num): Num { return num(a.v * b.v); }operator /(a: Num, b: Num): Num { return num(a.v / b.v); }operator %(a: Num, b: Num): Num { return num(a.v % b.v); }operator &(a: Num, b: Num): Num { return num(a.v & b.v); }operator |(a: Num, b: Num): Num { return num(a.v | b.v); }operator ^(a: Num, b: Num): Num { return num(a.v ^ b.v); }operator <<(a: Num, b: Num): Num { return num(a.v << b.v); }operator >>(a: Num, b: Num): Num { return num(a.v >> b.v); }operator ~(a: Num): Num { return num(~a.v); }operator ==(a: Num, b: Num): bool { return a.v == b.v; }operator !=(a: Num, b: Num): bool { return a.v != b.v; }operator <(a: Num, b: Num): bool { return a.v < b.v; }operator >(a: Num, b: Num): bool { return a.v > b.v; }operator <=(a: Num, b: Num): bool { return a.v <= b.v; }operator >=(a: Num, b: Num): bool { return a.v >= b.v; }fn main(): int {    immut a: Num = num(12);    immut b: Num = num(3);    test::assert((a + b).v == 15 && (a - b).v == 9, "+ -");    test::assert((a * b).v == 36 && (a / b).v == 4 && (a % b).v == 0, "* / %");    immut band: Num = a & b;    immut bor: Num = a | b;    immut bxor: Num = a ^ b;    immut shl: Num = a << b;    immut shr: Num = a >> b;    test::assert(band.v == 0 && bor.v == 15 && bxor.v == 15, "& | ^");    test::assert(shl.v == 96 && shr.v == 1, "<< >>");    test::assert((-a).v == -12 && (~a).v == ~12, "unary - and ~");    test::assert(a != b && !(a == b) && b < a && a > b && b <= a && a >= b, "comparisons");    return 0;}

Each comparison is its own operator. Declaring == does not give you !=, and declaring < does not give you <=; declare each one you use.

Not overloadable

  • && and ||. They short-circuit, which a function call cannot.
  • =, call (), member access ., and to conversions.
  • A compound assignment as an operator of its own. a += b is a = a + b and uses operator +; there is no operator +=. The same goes for ++ and --.

Declaring any of these is a syntax error (E110, Expected an overloadable operator after ‘operator’), and the hint lists the symbols that can be overloaded.

Operators between built-in types are not allowed

A declaration is rejected (E028) when every operand is a built-in primitive type (int, int32, double, bool, char, or a pointer): that would change how ordinary arithmetic means. At least one operand must be a struct, an enum or a string.

generics_and_operators_e028.tr
operator +(a: int, b: int): int {    return a;}fn main(): int {    return 0;}
Terminal
$ turf check generics_and_operators_e028.tr
generics_and_operators_e028.tr (1 error)

── error at line 1, col 1 [E028 · declaration]

  'operator +(int, int)' is not allowed.

  Hint: Both operand types are already built in -- overloading an operator between two builtin types would make ordinary arithmetic ambiguous. Operator overloads only make sense when at least one operand is a struct or 'string'.

  1 │ operator +(a: int, b: int): int {
    │ ^ Here!

  Run `turf --explain E028` for more details.

string is not on that list because its own operators (+, ==, !=, <, >, <=, >=) are ordinary overloads declared in the standard library’s prelude. They are always available, with no import:

generics_and_operators_operators_between_built.tr
import test;fn main(): int {    mut a: string = "foo";    mut b: string = "bar";    test::assert(a + b == "foobar", "concatenation is operator +");    test::assert(a == "foo", "equality compares content");    test::assert(a != b, "inequality");    test::assert(b < a, "ordering is lexicographic");    a += "!";    test::assert(a == "foo!", "a += b uses operator +");    return 0;}

Redeclaring operator +(string, string) yourself is an ambiguity error (E125). Enums are ordinary operand types: operator ==(a: Color, b: Color): bool is accepted, and is how you give an enum equality.

Overload selection

A use a op b looks for a visible operator op whose parameter types match the operand types exactly. Operators never widen: with operator +(a: Vec2, b: int) declared, v + 1 works and v + 1.0 is E057. Operand order matters too; 1 + v needs its own operator +(int, Vec2).

Overloads of the same symbol are told apart by their operand types:

generics_and_operators_overload_selection.tr
import test;struct Vec2 {    x: int,    y: int}fn vec(x: int, y: int): Vec2 {    mut r: Vec2 = Vec2::default();    r.x = x;    r.y = y;    return r;}operator +(a: Vec2, b: Vec2): Vec2 {    return vec(a.x + b.x, a.y + b.y);}operator +(a: Vec2, b: int): Vec2 {    return vec(a.x + b, a.y + b);}operator +(a: int, b: Vec2): Vec2 {    return vec(a + b.x, a + b.y);}fn main(): int {    immut a: Vec2 = vec(1, 2);    immut b: Vec2 = vec(10, 20);    immut s1: Vec2 = a + b;    immut s2: Vec2 = a + 5;    immut s3: Vec2 = 5 + a;    test::assert(s1.x == 11 && s1.y == 22, "Vec2 + Vec2");    test::assert(s2.x == 6 && s2.y == 7, "Vec2 + int");    test::assert(s3.x == 6 && s3.y == 7, "int + Vec2");    return 0;}
generics_and_operators_e057.tr
struct Vec2 {    x: int,    y: int}operator +(a: Vec2, b: int): Vec2 {    return a;}fn main(): int {    mut v: Vec2 = Vec2::default();    mut w: Vec2 = v + 1.0;    return 0;}
Terminal
$ turf check generics_and_operators_e057.tr
generics_and_operators_e057.tr (1 error)

── error at line 12, col 21 [E057 · arithmetic]

  Operator applied to the wrong kind of value!
  Type mismatch: cannot apply '+' to 'Vec2' and 'double'.

  Hint: '+' isn't a builtin operation on a struct -- declare 'operator +(Vec2, double): ReturnType { ... }', or check that an existing overload's parameter types match EXACTLY (operator resolution never widens int to double the way an ordinary function call does).

  12 │     mut w: Vec2 = v + 1.0;
     │                     ^ Here!

  Run `turf --explain E057` for more details.

The return type is yours to choose. Comparison operators usually return bool, but operator +(Vec2, int): int is legal.

Unary operators

-, ~ and ! take one parameter. A one-parameter operator - is negation; the two-parameter one is subtraction, and both can be declared for the same type.

!x on a value whose type declares operator ! calls that operator. For every other type, !x is the built-in x != true. bool never declares one, so ! on a bool is unaffected.

generics_and_operators_unary_operators.tr
import test;struct Vec2 {    x: int,    y: int}operator -(a: Vec2): Vec2 {    mut r: Vec2 = Vec2::default();    r.x = -a.x;    r.y = -a.y;    return r;}operator -(a: Vec2, b: Vec2): Vec2 {    mut r: Vec2 = Vec2::default();    r.x = a.x - b.x;    r.y = a.y - b.y;    return r;}struct Slot {    empty: bool}operator !(s: Slot): bool {    return s.empty;}fn main(): int {    mut v: Vec2 = Vec2::default();    v.x = 3;    v.y = -4;    immut neg: Vec2 = -v;    test::assert(neg.x == -3 && neg.y == 4, "unary -");    immut diff: Vec2 = v - neg;    test::assert(diff.x == 6, "binary - beside unary -");    mut s: Slot = Slot::default();    s.empty = true;    test::assert(!s, "!s calls operator !");    test::assert(!false, "!bool is unchanged");    return 0;}

Compound assignment, ++ and --

a op= b is a = a op b, with the left-hand location evaluated once, so a type that declares operator + supports +=. The result of the operator must have exactly the type of a. x++, x--, ++x and --x use operator + and operator - with an int right operand of 1; the prefix forms evaluate to the new value and the postfix forms to the old one. Any assignable location works: a variable, a field, an array element.

generics_and_operators_compound_assignment_and.tr
import test;struct Counter {    n: int}operator +(c: Counter, k: int): Counter {    mut r: Counter = c;    r.n = c.n + k;    return r;}operator -(c: Counter, k: int): Counter {    mut r: Counter = c;    r.n = c.n - k;    return r;}struct Tally {    total: Counter}fn main(): int {    mut c: Counter = Counter::default();    c.n = 10;    c += 5;    test::assert(c.n == 15, "+= uses operator +");    c -= 3;    test::assert(c.n == 12, "-= uses operator -");    ++c;    test::assert(c.n == 13, "prefix ++");    mut before: Counter = c++;    test::assert(before.n == 13 && c.n == 14, "postfix ++ yields the old value");    mut t: Tally = Tally::default();    t.total += 4;    test::assert(t.total.n == 4, "a field is a location too");    return 0;}

If the operator returns another type, a += b is E066 (the result “cannot be assigned back” to a); if no operator matches, it is E057.

Indexing: operator [] and operator []=

operator [] makes x[i] readable and operator []= makes x[i] = v writable. They are independent: declare only [] and the type is read-only; declare only []= and it is write-only (a read is then E126, Cannot index a value of type …). A use with an index or value type that matches no overload is E056 (No ‘operator[]’ overload matches …). The operator picked is the one whose parameter types match exactly, so a type can have several, for instance one indexed by int and one by string.

generics_and_operators_indexing_operator_and.tr
import test;struct Grid {    a: int,    b: int,    c: int}operator [](g: Grid, i: int): int {    if (i == 0) {        return g.a;    }    if (i == 1) {        return g.b;    }    return g.c;}operator []=(g: &Grid, i: int, value: int): void {    if (i == 0) {        g.a = value;    } else if (i == 1) {        g.b = value;    } else {        g.c = value;    }}operator []=(g: &Grid, name: string, value: int): void {    g.a = value;}fn main(): int {    mut g: Grid = Grid::default();    g[1] = 5;    g["first"] = 9;    g[2] = g[1] + 1;    test::assert(g[1] == 5 && g[0] == 9 && g[2] == 6, "read and write");    test::assert(g.c == 6, "the write reached g");    mut arr: int[3] = [1, 2, 3];    test::assert(arr[0] == 1, "built-in array indexing is unaffected");    return 0;}

The container of [] or []= may be declared by value (g: Grid) or by reference (g: &Grid); this is the one place operands are not matched exactly. Declare the container of []= as &Grid so the write reaches the caller’s variable; a by-value container would write into a copy. The call site is always just g[i] = v, and the reference is taken for you.

An index result is a value, not a place. x[i] calls a function that returns the element by value. Reading through it is fine (l[i].field, l[i].method()), but a write into part of the returned copy would be silently lost, so it is rejected with E098 instead:

generics_and_operators_e098.tr
import list::List;struct Big {    a: int,    b: int}fn main(): int {    mut bs: List<Big> = List::default();    mut e: Big = Big::default();    bs.push(e);    bs[0].a = 50;    return 0;}
Terminal
$ turf check generics_and_operators_e098.tr
generics_and_operators_e098.tr (1 error)

── error at line 12, col 13 [E098 · type mismatch]

  Cannot assign to part of the value an 'operator[]' read returned: it is a copy, so the write would be lost.

  Hint: 'x[i]' returns the element by value, so this would only change a copy. Read the element into a variable, change that, and store it back: `mut e: T = x[i]; e.field = v; x[i] = e;`. Writing THROUGH a pointer or reference element, or into a nested list (`grid[i][j] = v`), is fine.

  12 │     bs[0].a = 50;
     │             ^ Here!

  Run `turf --explain E098` for more details.

E098 also covers bs[0].a += 1, bs[0].a++, a call to a method that assigns to self (bs[0].bump()), and []= applied to another index’s result (mm["a"]["b"] = 1 on a Map<string, Map<string, int>>). Read the element into a variable, change it and store it back:

generics_and_operators_indexing_operator_and_2.tr
import list::List;import test;struct Big {    a: int,    b: int}fn main(): int {    mut bs: List<Big> = List::default();    mut e: Big = Big::default();    bs.push(e);    mut copy: Big = bs[0];    copy.a = 50;    bs[0] = copy;    test::assert(bs[0].a == 50, "stored back");    // These land in real storage, so they are accepted:    mut grid: List<List<int>> = List::default();    mut row: List<int> = List::default();    grid.push(row);    grid[0].push(4);    grid[0][0] = 8;    test::assert(grid[0][0] == 8, "a collection element is a shared handle");    return 0;}

The check is whether the destination is a field of the returned copy (or self of a mutating method called on it). A write through a pointer or reference element, or into a collection element (a handle to shared storage), is not a write to the copy and is allowed.

Compound assignment and ++/-- through an index are not supported (E126, Compound assignment through ‘operator[]’ is not supported). box[i] += 1 and box[i]++ must be written box[i] = box[i] + 1;.

generics_and_operators_e126.tr
struct Cell {    a: int}operator [](c: Cell, i: int): int {    return c.a;}operator []=(c: &Cell, i: int, value: int): void {    c.a = value;}fn main(): int {    mut c: Cell = Cell::default();    c[0] += 1;    return 0;}
Terminal
$ turf check generics_and_operators_e126.tr
generics_and_operators_e126.tr (1 error)

── error at line 15, col 10 [E126 · type mismatch]

  Operation not supported here!
  Compound assignment through 'operator[]' is not supported -- write 'x[i] = x[i] + int' explicitly instead.

  Hint: Write the operation out in full instead, e.g. `x = x + 1;`.

  15 │     c[0] += 1;
     │          ^~ Here!

  Run `turf --explain E126` for more details.

Generic operators

An operator can declare type parameters between the keyword and the symbol, exactly where fn<T> puts them. The standard collections index this way:

operator<T> [](l: List<T>, i: int): T
operator<T> []=(l: List<T>, i: int, value: T): void
operator<K, V> [](m: Map<K, V>, key: K): V
operator<K, V> []=(m: &Map<K, V>, key: K, value: V): void

so l[i], l[i] = v, m[k] and m[k] = v work for every List<T> and Map<K, V> (and s[i] reads a Set<T>; a Set has no []=). A generic struct of your own gets the same in any operator form. When < follows operator and an identifier, it opens a type-parameter list; operator<T> <(...) declares the less-than operator generically.

generics_and_operators_generic_operators.tr
import test;struct Cell<T> {    v: T,    n: int}fn<T> cell(v: T, n: int): Cell<T> {    mut c: Cell<T> = Cell::default();    c.v = v;    c.n = n;    return c;}operator<T> +(a: Cell<T>, k: int): Cell<T> {    return cell(a.v, a.n + k);}operator<T> -(c: Cell<T>): Cell<T> {    return cell(c.v, -c.n);}operator<T> <(a: Cell<T>, b: Cell<T>): bool {    return a.n < b.n;}operator<T> [](c: Cell<T>, i: int): T {    return c.v;}fn main(): int {    immut a: Cell<int> = cell(1, 5);    immut b: Cell<string> = cell("s", 2);    immut a2: Cell<int> = a + 3;    immut b2: Cell<string> = b + 1;    test::assert(a2.n == 8 && b2.n == 3, "one operator, two instantiations");    test::assert((-a).n == -5, "unary");    test::assert(a < a2 && b < b2, "<");    test::assert(a[0] == 1 && b[9] == "s", "[]");    mut c: Cell<int> = a;    c += 10;    c++;    test::assert(c.n == 16, "compound assignment");    return 0;}

A use resolves in two steps. First the type parameters are inferred from the operands (they cannot be written at a use). Then each declared operand type, with the parameters substituted, must match its operand exactly, as for a concrete operator. Generics add inference and nothing else: Cell<string> + 1.5 still fails against operator<T> +(a: Cell<T>, k: int) (E057), and Cell<int> == Cell<string> fails against operator<T> ==(a: Cell<T>, b: Cell<T>) because T is already int. One copy of the operator is compiled per distinct set of type arguments, as for a generic function.

Two rules apply to the declaration, both E097:

  • Every type parameter must appear in an operand. One that appears only in the return type could never be inferred.
  • At least one operand must be a struct or enum declared in the same module, looked for through &, * and the base of a generic type. A bare operator<T> +(a: T, b: T): T would apply to every type, including types from modules that never imported yours.
generics_and_operators_e097.tr
operator<T> +(a: T, b: T): T {    return a;}fn main(): int {    return 0;}
Terminal
$ turf check generics_and_operators_e097.tr
generics_and_operators_e097.tr (1 error)

── error at line 1, col 1 [E097 · declaration]

  'operator +' is generic but none of its operands is a type declared in this file.

  Hint: A bare type parameter matches every type, so the operator would apply to types this module does not own. Make an operand a struct or enum declared here, e.g. 'List<T>' in the module that declares List.

  1 │ operator<T> +(a: T, b: T): T {
    │ ^ Here!

  Run `turf --explain E097` for more details.

The orphan rule

Every operator declaration, generic or not, needs at least one operand type that is a struct or enum declared in the same module as the operator (E080, the operator “does not own any of its operand types”). Only operands made entirely of built-in types and string are exempt, since those have no owner module; they are the standard library’s own string operators. You can overload Vec2 + int in the module that declares Vec2, but not List<T> + List<T>, or List<int> + List<int>, anywhere else (an instance belongs to the module that declares its struct, and &/* around an operand do not change its owner): the meaning of a + b never depends on which modules happen to be imported.

generics_and_operators_e080.tr
import list::List;operator<T> +(a: List<T>, b: List<T>): List<T> {    return a;}fn main(): int {    return 0;}
Terminal
$ turf check generics_and_operators_e080.tr
generics_and_operators_e080.tr (1 error)

── error at line 3, col 1 [E080 · declaration]

  'operator +' does not own any of its operand types.

  Hint: 'list::List' is declared in module 'list', not in the main file -- at least one operand of an operator declaration must be a struct or enum declared in the same module/file as the operator itself, so an operator's meaning never depends on import order.

  3 │ operator<T> +(a: List<T>, b: List<T>): List<T> {
    │ ^ Here!

  Run `turf --explain E080` for more details.

Visibility

An operator is visible wherever its operand types are. There is no pub for an operator (writing one is E087; the hint says to make the type pub instead) and no qualified call syntax, so a + b works in any module that can name the type of a, whether it imported the type by name, the whole module, or only received a value from one of its functions. Import the module that declares the type and the operators come with it.

// vecmod.tr
pub struct Vec2 {
    pub x: int,
    pub y: int
}

pub fn make(x: int, y: int): Vec2 { ... }

operator +(a: Vec2, b: Vec2): Vec2 { ... }      // no `pub`

// main.tr
import vecmod::make;

fn main(): int {
    immut b = make(1, 2) + make(3, 4);          // operator + found
    return b.x;
}

A type’s own fields and methods still follow the usual pub rules; the operator’s body in vecmod can read private fields.

An operator used on a type with no matching overload is an ordinary type error (E057 for arithmetic, E124 for a comparison), never an internal one.

Summary of errors

Code Meaning
E028 Every operand is a built-in type
E056 No operator []/[]= matches the index or value type
E057 No operator matches these operand types (including a += with a mismatched operand)
E066 Result of the operator for a op= b is not the type of a
E080 The operator owns none of its operand types
E087 pub on an operator
E097 A generic operator has an unused type parameter, or owns no operand
E098 Writing into part of an index result
E107 / E115 Operator without a body / wrong number of parameters
E110 A symbol that cannot be overloaded
E124 No operator ==/</… for these operand types
E126 Indexing without operator [], or a compound assignment through one

Last updated: Oct 15, 2026Edit this page on GitHub

Esc

Type to search.