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 widthsi8…i128,u8…u128,f32,f64,isize,usize. Owned and borrowed byte sequences are[]byteand[..]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], neverList<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.