The prelude is installed by the compiler before your source is checked. Nothing
in it needs an import: string, []T, Map, Option, Task, File,
println, and the core traits are simply in scope.
fn main(): void {
names := ["Ada", "Grace", "Lin"]
for name in names.filter(n => n.length() > 3).sorted() {
println("Hello, ${name}!")
}
}Most of the prelude is written in Atoll itself, in the stub files under
crates/atoll-sema/src/stubs/. Those files are the authoritative signature
inventory, and generic bodies specialize against your code exactly like any
other Atoll source.
What is in scope
| Area | Types |
|---|---|
| Scalars | bool, int, i8…i128, u8…u128, isize, usize, float, f32, char, byte |
| Text and binary | string, substring, []byte, [..]byte |
| Collections | List, Slice, Map, SortedMap, Set, SortedSet, Range |
| Outcomes | Option, Result |
| Concurrency | Task, Stream, Selectable |
| Time | Duration, DateTime, Date, Time, TimeZone, ZonedDateTime |
| Host I/O | File, Dir, Http, WebSocket, Tcp |
| Traits | Equatable, Hashable, Comparable, Display, Numeric, Iterator, Iterable, Bytes, Buffer, Clone, Default, Drop, and the operator traits |
Scalar type names stay lowercase; nominal types and traits are PascalCase;
methods are snake_case.
Scalars and text
Integer arithmetic wraps rather than trapping, and wrapping_add is the
explicit spelling of that. Text separates character counts from byte counts, and
every accessor that could run off the end returns an Option.
fn main(): void {
count: u8 = 250u8
println("${count.wrapping_add(10u8)}") // 4
name := "Ångström"
println("${name.length()} chars, ${name.len()} bytes") // 8 chars, 10 bytes
println("${name.char_at(0u32) ?? '?'}")
}See Numbers, Strings, and Bytes.
Collections
[]T is the workhorse. The whole higher-order surface — map, filter,
fold, sorted_by, distinct, sum_of — lives on it directly, so there is
no separate iterator type to opt into.
struct Sale { region: string, cents: int }
fn totals(sales: []Sale): []string {
mut out: []string = []
for region in sales.map(s => s.region).distinct().sorted() {
total := sales.filter(s => s.region == region).sum_of(s => s.cents)
out.add("${region}: ${total}")
}
return out
}
fn main(): void {
sales := [
Sale { region: "emea", cents: 12000 },
Sale { region: "amer", cents: 8050 },
Sale { region: "emea", cents: 4550 },
]
for line in totals(sales) {
println(line)
}
}Map and Set cover keyed and unique storage, and both iterate with for.
fn word_counts(words: []string): Map[string, int] {
mut counts: Map[string, int] = Map.new()
for word in words {
counts.put(word, (counts.get(word) ?? 0) + 1)
}
return counts
}
fn unique(words: []string): Set[string] {
mut seen: Set[string] = Set.new()
for word in words {
seen.add(word)
}
return seen
}
fn main(): void {
words := ["a", "b", "a"]
println("${word_counts(words).get("a") ?? 0} ${unique(words).size()}")
}See Lists, Maps, Sets, and Iteration.
Absence and failure
The standard library has no hidden panics. Absence is Option[T] and failure
is Result[T, E], spelled T ! E in a return type. ?? supplies an Option
fallback and ? propagates a Result.
error ConfigError { Missing { key: string }, NotANumber { key: string } }
fn lookup(settings: Map[string, string], key: string): string ! ConfigError {
match settings.get(key) {
Some(value) => return value
None => error Missing { key: key }
}
}
fn read_port(settings: Map[string, string]): int ! ConfigError {
raw := lookup(settings, "port")?
match raw.trim().to_int() {
Some(port) => return port
None => error NotANumber { key: "port" }
}
}
fn read_port_or_default(settings: Map[string, string]): int {
return read_port(settings).unwrap_or(8080)
}
fn main(): void {
mut settings: Map[string, string] = Map.new()
settings.put("port", "9000")
println("${read_port_or_default(settings)}")
}Numeric edges follow the same rule: parsing returns Option, and out-of-range
collection access returns Option rather than trapping.
Traits are the extension points
Operators and generic algorithms dispatch through prelude traits. Implementing
one gives your type the corresponding syntax — there is no Add trait, for
instance; arithmetic comes from Numeric.
struct Money { cents: int }
impl Display for Money {
fn to_string(self): string {
pad := if (self.cents % 100).abs() < 10 { "0" } else { "" }
return "${self.cents / 100}.${pad}${(self.cents % 100).abs()}"
}
}
fn main(): void {
// Display backs string interpolation.
println("total ${Money { cents: 1250 }}")
}@derive generates the mechanical ones, which is what makes a type usable as a
Map key or a Set element:
@derive(Equatable, Hashable)
struct Point { x: int, y: int }
fn main(): void {
mut labels: Map[Point, string] = Map.new()
labels.put(Point { x: 1, y: 2 }, "waypoint")
println(labels.get(Point { x: 1, y: 2 }) ?? "missing")
}Ordering is the exception: sorted() and sorted_descending() need a real
Comparable[T] impl, and @derive(Comparable) does not satisfy that bound
today.
struct Version { major: int, minor: int }
impl Comparable[Version] for Version {
fn compare_to(self, other: Version): int {
if self.major != other.major { return self.major - other.major }
return self.minor - other.minor
}
}
fn main(): void {
vs := [Version { major: 1, minor: 4 }, Version { major: 2, minor: 0 }]
match vs.sorted_descending().first() {
Some(v) => println("newest ${v.major}.${v.minor}")
None => println("none")
}
}See Traits.
Concurrency and time
spawn yields a Task[T] you await or cancel; Stream carries values between
tasks; Duration and DateTime are ordinary values.
fn work(n: int): int {
return n * n
}
fn main(): void {
a := spawn { work(6) }
b := spawn { work(7) }
println("${a.await() + b.await()}")
budget := Duration.of_millis(250)
println("${budget.total_millis()}ms")
}Host operations
Method syntax does not imply an in-memory call. File.read, Http.get, and
Tcp.connect are namespace methods backed by host operations: they can
suspend, and they can be denied by the deployment’s capability policy even
after they type-check.
fn line_count(path: string): int {
match File.read(path) {
Ok(text) => return text.split("\n").len()
Err(e) => return 0
}
}
fn load(path: string): string ! IoError {
return File.read(path)?
}
fn main(): void {
println("${line_count("/etc/hostname")}")
match load("/etc/hostname") {
Ok(text) => println("${text.len()} bytes")
Err(e) => println("unreadable")
}
}Host handles — open files, HTTP bodies, clients, listeners, sockets — represent
runtime-owned state. The discipline is: acquire through a fallible constructor,
keep one owning scope or task, bound reads and concurrent children, close
explicitly when early release matters, and otherwise let Drop run on return,
error propagation, or cancellation. Never use a handle after closing it, and
never assume copying it duplicates the underlying resource.
Automatic cleanup prevents leaks; it does not define protocol. Dropping a socket releases it but does not prove a response was flushed, a WebSocket close handshake completed, or a transaction committed.
Reading a prelude signature
When you meet an unfamiliar prelude method, four questions settle how to use it:
- Does it return
OptionorResult? That is the absence or failure contract, and the compiler will make you discharge it. - Does it return owned storage or a borrowed view?
to_bytes()andto_string()own;bytes()andslice()borrow the parent and must be used inside its scope. - Can it suspend? Host operations can, which constrains what may be held across the call.
- Does it lower? Open
crates/atoll-sema/src/stubs/and look at the declaration. Afnwith a{ ... }body, an@intrinsic, or an@hostimport will compile. Afnwhose signature ends without a body is a semantic-checking stub and will not — the failure surfaces atatoll build, never atatoll check.
Question 4 is the one that costs time. list.at illustrates all three cases in
adjacent lines:
fn distinct(self): []T {
// ... real Atoll body: compiles and specializes like your code
}
fn join(self, separator: string): string
fn join_of(self, separator: string, f: fn(T) -> string): stringdistinct compiles. join and join_of type-check and then fail
ATOLL2004. Each chapter page’s Availability table records the current
state so you do not have to read the stubs yourself.
Names that are not builtins
The older builtin directory lists names the current compiler does not install.
There is no prelude Any, Email, Phone, Queue, Stack, Url, or
Money, and the old nominal bytes and Buffer structs were replaced by
[]byte, [..]byte, and the Bytes/Buffer traits. Stack- and queue-shaped
logic uses []T; domain types are yours to define.
struct Stack[T] {
items: []T
fn push(mut self, value: T): void {
self.items.add(value)
}
fn peek(self): T? {
return self.items.last()
}
fn depth(self): int {
return self.items.len()
}
}
fn main(): void {
mut s := Stack { items: [1, 2] }
s.push(3)
println("${s.peek() ?? 0} ${s.depth()}")
}