* Documentation

Language Tour

The full Turf language surface: types, control flow, structs, enums, error handling, generics, modules, FFI, and state machines.

Turf’s compiler pipeline is a standard deterministic front-to-back: lexer → parser → resolver → type checker → codegen, LLVM backend. This page covers everything you can write. For the full per-construct reference and edge cases, see docs/reference/ in the repo; for compiler internals, see Compiler Architecture.

Variable declarations

Every declaration is explicitly typed and explicitly mut or immut:

mut x: int = 10;
immut pi: double = 3.14159;
mut flag: bool = true;
mut name: string = "Turf";

Assigning to an immut binding, or reading a variable before it’s definitely assigned on every path, is a compile error (DefiniteAssignmentError) — checked by real dataflow analysis over the function’s control-flow graph, not just “assigned somewhere in the body.”

Casting

Explicit conversions use the to operator:

mut a: double = 42 to double;
mut b: int = 3.75 to int;      // truncates: 3
mut c: int = "100" to int;     // parses the string
mut s: string = 42 to string;

to binds tighter than assignment but looser than everything else, so y = x to int; parses as y = (x to int).

Operators

Precedence, loosest to tightest binding:

PrecedenceOperatorsAssociativity
10= += -= *= /= %= &= |= ^= <<= >>= **=right
12to (cast)left
15||left
20&&left
25== !=left
30< > <= >=left
33| (bitwise OR)left
35^ (bitwise XOR)left
37& (bitwise AND)left
38<< >>left
40+ - (binary)left
50* / %left
60** (exponentiation)right

Notable departures from C-family intuition:

Bitwise operators: &, |, ~, ^ (XOR), <<, >>.

mut flags: int = 0b1010 & 0b1100;
mut shifted: int = 1 << 4;

Operator overloading

Operators are declared at the top level with the operator keyword — never .- or ::-qualified, never generic, never static:

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;
}

Only ten binary symbols are overloadable: + - * / == != < > <= >=. Deliberately not overloadable: %, **, every bitwise operator, &&/||, and any assignment form (including compound assignment). There’s no indexing-operator overload (operator [](...)) and no unary-operator overload — every operator declaration takes exactly two parameters.

An operator is rejected if both operand types are already a built-in primitive (int, int32, double, bool, char, or a pointer type) — you can’t redefine int + int. string is deliberately excluded from that builtin list: string’s own +, ==, !=, <, >, <=, >= are ordinary operator overloads living in lib/std/core/strings.tr, which is why you need import strings; to use them.

Control flow

Block form:

if x > 0 {
    io::printline("positive");
} elseif x < 0 {
    io::printline("negative");
} else {
    io::printline("zero");
}

compare is Turf’s switch statement — each matches arm ends with done, and an optional otherwise arm must come last:

compare x {
    matches 1:
        io::printline("one");
    done
    matches 2:
        io::printline("two");
    done
    otherwise:
        io::printline("something else");
}

Ternary (expression) form uses give / otherwise:

mut max: int = a > b give a otherwise b;

Loops:

while i < 10 {
    i = i + 1;
    if i % 2 == 0 { continue; }
}

for j in 1..10 step j += 1 {
    if j % 2 == 0 { continue; }
    sum = sum + j;
}

do {
    i = i - 1;
} while (i > 0);   // body always runs at least once

Structs

Fields are name: Type, comma-separated. There is no brace-literal construction syntax — every struct is built via exactly one of two reserved, compiler-recognized calls:

struct User {
    age: int,
    name: string
}

fn User.new(age: int, name: string): void {
    self.age = age;
    self.name = name;
}

mut u: User = User::new(10, "Alice");
u.age = 25;

mut u2: User = User::default();   // every field recursively zero-initialized

new and default are reserved struct-method names, not ordinary functions:

Other instance methods are declared the same way as new, called via ., with self implicit:

fn User.grow(): void {
    self.age = self.age + 1;
}

fn User.rename(new_name: string): void {
    self.name = new_name;
}

u.grow();
u.rename("Bob");

Static methods use the exact same . declaration form, distinguished only by a trailing static keyword (no implicit self) — but are called on the type itself via ::, never through an instance:

fn User.describe(prefix: string) static: string {
    return prefix + " user";
}

io::printline(User::describe("a"));

:: never appears in a fn declaration — it’s reserved for calling/referencing through a namespace (enum variants, module-qualified functions, static methods, new/default), while . is used for every method declaration, static or not.

A struct with no fields at all (struct Empty {}) is fully supported — the natural shape for a “namespace” struct that only ever holds static methods.

Arrays

Fixed-size, typed, and index directly — including arrays of structs, with field access chaining through the index:

mut users: User[5] = [
    User::default(),
    User::default(),
    User::default(),
    User::default(),
    User::default()
];

users[0] = User::new(25, "Alice");
assert(users[0].age == 25, "");

Bounds checking happens both at compile time (when the index is a statically-known constant — ArrayBoundsError, code E001) and at runtime otherwise.

Enums

:: addresses a variant, the same operator used for every other namespace-qualified reference in the language:

enum Color { Red, Green, Blue }

mut c: Color = Color::Green;
if c == Color::Green {
    io::printline("green");
}

Color::COUNT, Color::VALUES, and Color::NAMES are built in for every enum — variant count, the array of underlying values, and the array of variant name strings, respectively.

Error handling

Recoverable errors are named error types — bare-name variants only, no payload fields:

error MathError {
    DivisionByZero
}

fn divide(a: int, b: int): !int {
    if b == 0 {
        return MathError::DivisionByZero;
    }
    return a / b;
}

A function returning !T can’t be used as a plain T at the call site — it must be handled with recover, in one of three forms:

mut x: int = divide(10, 0) recover produce 0;   // fallback value on failure

fn tryDivide(a: int, b: int): int {
    return divide(a, b) recover as err run {     // bind the specific error variant
        if err == MathError::DivisionByZero {
            io::printline("division by zero");
        }
        return -1;
    };
}

recover run/recover as err run blocks must produce the success type on every path (usually via return) — the one exception is a !void success type, where the block can just run side effects and fall through:

extern fn writeFile(path: string, contents: string): !void;

writeFile("a.txt", "hi") recover run {
    io::printline("write failed");
};

recover as err run { ... } requires the callee to declare a real error type — a bare error GeneralError; type (no variants beyond the implicit GeneralError::DEFAULT) can still use recover produce/recover run, just not recover as.

First-class functions

An unqualified fn name is a value — it can be passed as an argument, stored in a variable, and called indirectly:

fn apply(f: fn(int): int, x: int): int {
    return f(x);
}

fn double_it(n: int): int { return n * 2; }

mut result: int = apply(double_it, 5); // 10

Only bare, unqualified function names are first-class today — Module::func and StructName::method referenced as a value (not called) still raise a compile error.

Generics

Turf uses real generics (Name<T1, T2>) rather than a single hardcoded collection type — both user-defined generic structs and the standard library’s own collections work the same way:

import io;
import list::List;

struct Box<T> {
    val: T
}

fn Box.new(v: T): void {
    self.val = v;
}

fn Box.get(): T {
    return self.val;
}

fn main(): int {
    mut b: Box<int> = Box::new(42);
    assert(b.get() == 42, "");

    mut nums: List<int> = List::default();
    nums.push(10);
    nums.push(20);
    assert(nums.length() == 2, "");

    return 0;
}

A generic method with its own type parameters puts the <T> block right after fn, before the struct-dot method name (fn<T> Box.set(v: &T): void { ... }).

There is no explicit-type-argument construction syntax — you never write Box<int>::new(...). < after an identifier in expression position always parses as less-than; the concrete type argument is inferred entirely from the declared type of the variable you’re assigning into.

Type erasure, not monomorphization: every instantiation of a generic struct shares the exact same underlying layout — a field typed with the struct’s own type parameter erases to a uniform pointer-sized slot regardless of the concrete type, and a generic method’s body is type-checked and codegenned exactly once, shared across every instantiation. This is what makes generics cheap in this compiler, and it’s also the source of the one documented stdlib limitation (see Standard Library).

Modules & Imports

:: is the general namespace-resolution operator — enum variants, module-qualified functions, module-qualified types, and static methods all use it. . stays reserved for instance-member access only.

Every source file — the standard library’s own modules included — is an ordinary .tr file with no compiler special-casing. import has four forms:

import io;                       // qualified access only: io::printline(...)
import io::printline;            // + bare access to just that one symbol
import io::[printline, print];   // + bare access to exactly these symbols
import io::[*];                  // + bare access to everything the module exports

A plain import Module; always grants qualified access (Module::thing) — the on-demand forms only add bare, unqualified access to specific symbols on top of that. This applies uniformly to functions, structs, and enums. Circular imports are detected and reported with the full import chain.

Functions, structs, and enums are exportable automatically — anything declared at a file’s top level is visible to importers with no keyword needed. A top-level constant needs export:

export immut PI: double = 3.14159;
export immut MAX_USERS: int = 100;

export only works on immut declarations at the true top level, and the initializer must be a literal — an exported constant has no real storage; every reference to it is replaced by its value directly at compile time.

FFI

extern fn malloc(size: int): ptr;
extern fn free(p: ptr): void;
extern fn strlen(s: string): int;
extern from "m" fn sqrt(x: double): double;   // links -lm automatically

fn main(): int {
    unsafe {
        mut buf: ptr = malloc(64);
        free(buf);
    }
    return 0;
}

unsafe { } and safe references (&T)

Raw pointer types (ptr, T*) are gated behind unsafe { } everywhere outside lib/std — declaring a raw-pointer-typed variable, casting to/from one, or accessing a field through one all require lexically being inside unsafe { }. A struct with a raw pointer field needs its entire declaration wrapped, not just the code that dereferences it later. An ordinary (non-extern) function’s parameters and return type may never be raw pointers at all, even inside unsafe { } — extern fn bindings and anything under lib/std are exempt.

&T is a compiler-guaranteed non-null reference — &expr (address-of) always produces one:

mutable val: int = 42;
mutable ref: &int = &val;   // compiler-guaranteed non-null

A &T auto-derefs on field/member access exactly like a raw pointer does, and always safely widens into a raw T*/ptr with no cast or unsafe { } needed. The reverse — turning a raw pointer into a &T — always needs an explicit to cast inside unsafe { }, since the raw pointer might be null:

unsafe {
    mutable p: Node* = ...;
    mutable safe: &Node = p to &Node;   // asserts p isn't null
}

State machines

machine declares a set of states plus a transition graph in one construct — a TurfMachineType is a real enum underneath (::-access, ==, COUNT all just work), with a .transition(State) call the compiler tries to prove safe at compile time:

machine TrafficLight {
    Red -> Green,
    Green -> Yellow,
    Yellow -> Red
}

fn main(): int {
    mut light: TrafficLight = TrafficLight::Red;

    compare light {
        matches TrafficLight::Red:
            light.transition(TrafficLight::Green);   // compile-time-proven safe: zero runtime cost
        done
        otherwise:
            io::printline("not red");
    }

    assert(light == TrafficLight::Green, "");
    return 0;
}

Inside a single-pattern matches arm that’s narrowed the receiver to one exact state, .transition(...) compiles to a plain store — no runtime check at all. Everywhere else it falls back to a runtime switch that panics on an edge the graph doesn’t declare.

Assertions

assert(condition, "message shown on failure");

Where to go next