Skip to content

Standard Library

A tour of the compiler-installed Atoll prelude — scalars, collections, outcomes, concurrency, and host I/O.

Updated View as Markdown

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.

See Option and Result.

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.

See Files, HTTP, and TCP.

Reading a prelude signature

When you meet an unfamiliar prelude method, four questions settle how to use it:

  1. Does it return Option or Result? That is the absence or failure contract, and the compiler will make you discharge it.
  2. Does it return owned storage or a borrowed view? to_bytes() and to_string() own; bytes() and slice() borrow the parent and must be used inside its scope.
  3. Can it suspend? Host operations can, which constrains what may be held across the call.
  4. Does it lower? Open crates/atoll-sema/src/stubs/ and look at the declaration. A fn with a { ... } body, an @intrinsic, or an @host import will compile. A fn whose signature ends without a body is a semantic-checking stub and will not — the failure surfaces at atoll build, never at atoll 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): string

distinct 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()}")
}

Chapter map

Navigation

Type to search…

↑↓ navigate↵ selectEsc close