Skip to content

Types

Atoll's static type system, type forms, inference, and user-defined types.

Updated View as Markdown

Atoll is statically typed. The compiler fixes one concrete type for every expression before lowering, and infers local types when the surrounding program provides enough information.

count := 3
name := "Nori"
pair: (string, int) = (name, count)
maybe_name: string? = Some(name)
println("${pair.0} ${pair.1} ${maybe_name ?? "-"}")

Spelling

Three rules cover almost every type you will write:

  • Scalar and text builtins are lowercase: int, float, bool, char, byte, string, substring, plus the fixed widths i8…i128, u8…u128, f32, f64, isize, usize. Owned and borrowed byte sequences are []byte and [..]byte.
  • Nominal and composite types are PascalCase: List, Map, Set, Option, Result, Stream, Task, and everything you declare.
  • Generic arguments use square brackets: List[int], never List<int>.
struct Cache {
    entries: Map[string, int]
    recent: []string
    limit: usize
}

fn main(): void {
    mut c := Cache { entries: Map.new[string, int](), recent: [], limit: 16usize }
    c.entries.put("hits", 3)
    c.recent.add("hits")
    println("${c.entries.len()} ${c.recent.len()} ${c.limit.to_int()}")
}

Type forms

Form Meaning Where it may be written
int, string, User Named type anywhere
List[T] or []T Growable owned list anywhere
Slice[T] or [..]T Borrowed list view anywhere
[N]T Fixed-length array anywhere
(A, B) Tuple anywhere
fn(A, B) -> R Function value anywhere
T? Option[T] anywhere
&T Reference type anywhere
A | B Anonymous enum anywhere
{ name: string, age: int } Anonymous record anywhere
impl Trait Opaque value implementing a trait anywhere
! Never type anywhere
T ! E Result[T, E] function return type only

The last row is the one that surprises people. T ! E is signature sugar, not a general type form: the parser accepts ! only directly after a function’s return type. Everywhere else — a parameter, a struct field, a binding annotation — write Result[T, E].

error LoadError { NotFound }

fn load(id: int): int ! LoadError => id

fn main(): void {
    v: int ! LoadError = load(1)
    println("done")
}

That is ATOLL1021: \!` is only valid after a function’s return type`. The long spelling works in every position:

error LoadError { NotFound }

fn load(id: int): int ! LoadError {
    if id < 0 { error NotFound }
    return id
}

fn describe(outcome: Result[int, LoadError]): string {
    return match outcome {
        Ok(v) => "ok ${v}"
        Err(e) => "failed"
    }
}

fn main(): void {
    stored: Result[int, LoadError] = load(7)
    println("${describe(stored)} ${describe(load(-1))}")
}

A tour of the forms

Each form in one compiling unit, so you can see how they read in context:

struct User { id: int, name: string }
struct Robot { serial: string }
error LoadError { NotFound }

fn sum(scores: [..]int): int {
    mut total := 0
    for s in scores { total = total + s }
    return total
}

fn head(users: []User): User? => users.get(0)

fn load(id: int): User ! LoadError {
    if id < 0 { error NotFound }
    return User { id: id, name: "ada" }
}

fn mapped(xs: []int, f: fn(int) -> int): []int => xs.map(f)

fn who(value: User | Robot): string {
    return match value {
        User { name, id: _ } => "user ${name}"
        Robot { serial } => "robot ${serial}"
    }
}

fn stats(u: User) => { id: u.id, letters: u.name.len() }

fn main(): void {
    users: []User = [User { id: 1, name: "ada" }]
    fixed: [3]int = [10, 20, 30]
    pair: (string, int) = ("count", users.len())
    scores := [1, 2, 3]

    println("${pair.0}=${pair.1} second=${fixed[1] ?? 0}")
    match scores[0..2] {
        Some(view) => println("slice sum ${sum(view)}")
        None => println("range out of bounds")
    }
    println("${head(users)?.name ?? "-"}")
    match load(1) {
        Ok(u) => println("${who(u)} / ${stats(u).letters} letters")
        Err(e) => println("missing")
    }
    println("${mapped(scores, n => n * 2).len()} ${who(Robot { serial: "R2" })}")
}

How checking proceeds

Checking is bidirectional. Declared parameter, field, and return types push an expected type inward; literals, calls, and operators push a concrete type outward. The two meet, and anything left unresolved is a diagnostic rather than a runtime decision.

struct Server { port: u16, hostname: string }

fn listen(s: Server): int => s.port.to_int()

fn main(): void {
    // `8080` is checked directly as u16 — no int is created and narrowed.
    s := Server { port: 8080, hostname: "localhost" }
    println("${listen(s)}")
}

Because the literal is checked against the field’s declared type, an out-of-range value is caught before any code runs:

struct Server { port: u16, hostname: string }

fn main(): void {
    s := Server { port: 70000, hostname: "localhost" }
    println("${s.hostname}")
}

Branches are joined rather than checked independently, and the never type ! contributes nothing to a join. That is why an arm which leaves the function can sit beside an arm that produces a value:

fn parse_or_bail(text: string): int {
    value := match text.to_int() {
        Some(v) => v
        None => return -1        // type `!`
    }
    return value * 2
}

fn main(): void {
    println("${parse_or_bail("21")} ${parse_or_bail("nope")}")
}

Choosing a form

Pick the form that exposes the invariant callers need, not the shortest spelling — representation also decides ownership, method lookup, pattern matching, and how the API can evolve.

Need Prefer
Ordered growable owned sequence []T or List[T]
Borrowed contiguous sequence view [..]T or Slice[T]
Fixed element count in the type [N]T
Named product with stable identity struct
Structural product local to a boundary record
Named closed alternatives enum
Canonical structural alternatives anonymous union
Readability without new identity transparent alias
Domain separation over a representation distinct alias
Legitimate absence Option[T] or T?
Typed operational failure Result[T, E], spelled T ! E in a signature
struct Point { x: int, y: int }

fn area(a: Point, b: Point): int {
    return (b.x - a.x).abs() * (b.y - a.y).abs()
}

fn main(): void {
    // A tuple would carry the same two ints with none of the meaning.
    println("${area(Point { x: 0, y: 0 }, Point { x: 3, y: 4 })}")
}

Chapters

  • Primitives defines the scalar, text, unit, never, and machine-width types built into the compiler, with ranges, literal spellings, and arithmetic behaviour.
  • Inference explains expected types, contextual literals, branch joins, and exactly where an annotation is mandatory.
  • Conversions separates contextual typing, implicit lifts, explicit casts, parsing, and domain conversion.
  • Generics covers type parameters, bounds, explicit call arguments, opaque types, and specialization.
  • Structs and Records distinguish nominal and structural product types.
  • Enums and Unions distinguish named and structural sum types.
  • Aliases and Projections name existing or inferred types.
  • Tuples and Sequences cover positional products, lists, arrays, and slices.
  • Traits and Implementations define static behavior and dispatch.
  • Values and References explain copying, sharing, mutation, managed lifetimes, safe references, and raw pointers.

Older documents use names such as Int, Float, Boolean, String, and angle-bracket generics. They are historical spellings and are not used in this guide.

Continue with Errors once optional and fallible carriers are familiar, and with Standard Library for the installed methods and trait contracts on these forms.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close