A declaration introduces a named program item at the top level of a file. Everything an Atoll file contains is a declaration — there is no top-level statement, no top-level expression, and no module initializer.
const DEFAULT_PORT: u16 = 8080
struct Server {
host: string
port: u16
}
fn start(server: Server): string {
return "${server.host}:${server.port}"
}
fn main(): void {
println(start(Server { host: "localhost", port: DEFAULT_PORT }))
}Code that is not inside a declaration is rejected before name resolution even begins:
x := 1
fn main(): void {
println("${x}")
}The declaration forms
Every core top-level form appears in this one unit. Each is described in its own section below.
const RETRY_BUDGET: int = 3
struct Server { host: string, port: u16 }
enum State { Idle, Running, Done }
error StartError { PortInUse, Denied }
type Port = u16
distinct type ServerId = int
trait Describe {
fn describe(self): string
}
impl Describe for Server {
fn describe(self): string => "${self.host}:${self.port}"
}
fn start(server: Server, state: State): string ! StartError {
if server.port == 0 {
error PortInUse
}
match state {
Running => return server.describe()
Idle => error Denied
Done => error Denied
}
}
fn main(): void {
port: Port = 8080
server := Server { host: "localhost", port: port }
println("budget ${RETRY_BUDGET}")
println(start(server, Running).unwrap_or("not running"))
println(start(server, Idle).unwrap_or("not running"))
}| Form | Introduces |
|---|---|
fn |
A function, method, or receiver-prefixed method |
const |
A compile-time constant value |
struct |
A nominal product type |
enum |
Named closed alternatives |
error |
Error variants, or a union of error types |
type |
A transparent alias |
distinct type |
A new nominal type over an existing representation |
trait |
A statically dispatched behaviour contract |
impl |
Inherent or trait behaviour for a type |
schema |
The logical SQL schema a unit’s models belong to |
model |
A SQL-backed nominal row type |
module and import are file items rather than declarations: they name the
file’s module and pull names in, but they do not introduce a value or a type.
Words such as service, actor, job, component, and guard appear in
older design documents. They are not accepted declaration keywords.
Constants
const binds a name to a value that the compiler folds before code
generation. The type annotation is optional when the initializer determines it.
const MAX_RETRIES: int = 3
const TIMEOUT_MS = 250
const GREETING: string = "hello"
const SAMPLE_RATE: float = 0.25
const TRACING: bool = false
const MAX_ATTEMPTS: int = MAX_RETRIES * 2
fn main(): void {
println("${GREETING} retries=${MAX_ATTEMPTS} rate=${SAMPLE_RATE} ms=${TIMEOUT_MS} trace=${TRACING}")
}Constants may refer to earlier constants, because the whole expression is evaluated at compile time. Calling a function is not const-evaluable, and the compiler says so:
fn now(): int => 7
const STARTED_AT: int = now()Constant names use SCREAMING_SNAKE_CASE. Use one when the value is genuinely
fixed for the program; use a static function when the value should be
constructed at run time.
Types
struct declares a product type, enum a closed set of alternatives, and
error a set of error variants. All three are nominal: two declarations with
identical contents remain different types.
struct Reading {
sensor: string
celsius: float
}
enum Severity { Info, Warn, Critical }
enum Sample {
Missing
Point(float)
Span(float, float)
}
error ReadError { Offline, OutOfRange }
fn classify(s: Sample): Severity {
match s {
Missing => return Critical
Point(v) => if v > 100.0 { return Warn } else { return Info }
Span(lo, hi) => if hi - lo > 50.0 { return Warn } else { return Info }
}
}
fn main(): void {
r := Reading { sensor: "boiler", celsius: 91.5 }
match classify(Point(r.celsius)) {
Info => println("${r.sensor} ok")
Warn => println("${r.sensor} warn")
Critical => println("${r.sensor} critical")
}
}Enum variants carry positional payloads written in parentheses. Full detail lives in Structs and Enums.
An error declaration can also flatten several error types into one:
error StartError { PortInUse, Denied }
error IoError { NotFound, Truncated }
error AppError = StartError | IoError
fn main(): void {
println("declared")
}Aliases
type introduces a transparent alias — the alias and its target are the same
type, and values pass freely between them.
type Port = u16
type Row = { id: int, label: string }
fn bind(p: Port): string => "bound ${p}"
fn describe(r: Row): string => "${r.id}:${r.label}"
fn main(): void {
println(bind(8080))
println(describe({ id: 1, label: "first" }))
}distinct type introduces a new nominal type that shares a representation
with its target but does not interchange with it. That is the point: it turns
a stringly- or numerically-typed API into a checked one.
distinct type ServerId = int
distinct type TenantId = int
struct Assignment {
server: ServerId
tenant: TenantId
}
fn assign(server: ServerId, tenant: TenantId): Assignment {
return Assignment { server, tenant }
}
fn main(): void {
println("declared")
}Passing a bare int where a distinct type is expected is an error, which is
exactly what makes the two ids impossible to swap:
distinct type ServerId = int
fn label(id: ServerId): string => "server"
fn main(): void {
println(label(1))
}That nominality currently has no escape hatch: there is no conversion form
that turns a representation value into a distinct type value, so a
distinct type over a primitive can be declared and threaded through
signatures but not constructed. Reach for a one-field struct when you need
both the nominal guarantee and a way to build a value today.
struct ServerId { value: int }
struct TenantId { value: int }
struct Assignment {
server: ServerId
tenant: TenantId
}
fn assign(server: ServerId, tenant: TenantId): Assignment =>
Assignment { server, tenant }
fn main(): void {
a := assign(ServerId { value: 7 }, TenantId { value: 3 })
println("${a.server.value} -> ${a.tenant.value}")
}Behaviour
trait declares requirements, optionally with default bodies, associated
types, and associated constants. impl supplies them for a concrete type.
trait Shape {
fn area(self): float
fn name(self): string => "shape"
}
struct Square { side: float }
struct Circle { radius: float }
impl Shape for Square {
fn area(self): float => self.side * self.side
fn name(self): string => "square"
}
impl Shape for Circle {
fn area(self): float => 3.14159 * self.radius * self.radius
}
fn report[T](s: T): string where T: Shape => "${s.name()} ${s.area()}"
fn main(): void {
println(report(Square { side: 2.0 }))
println(report(Circle { radius: 1.0 }))
}Circle inherits the default name. Dispatch is static: report is checked
against the Shape bound and specialized per concrete type — there is no
dyn Shape.
An impl block without a trait supplies inherent behaviour:
struct Vec2 { x: float, y: float }
impl Vec2 {
fn zero(): Vec2 => Vec2 { x: 0.0, y: 0.0 }
fn scaled(self, k: float): Vec2 => Vec2 { x: self.x * k, y: self.y * k }
fn length(self): float => (self.x * self.x + self.y * self.y).sqrt()
}
fn main(): void {
v := Vec2 { x: 3.0, y: 4.0 }
println("${v.length()} ${v.scaled(2.0).x} ${Vec2.zero().x}")
}See Traits and Implementations.
Data
A unit that runs SQL needs a schema declaration naming the logical schema,
plus a model declaration for each row type. Models are ordinary nominal
types that additionally describe a table.
schema shop
model Item {
@id
id: int
name: string
price: float
}
fn affordable(limit: float): []{ id: int, name: string } ! SqlError {
return FROM Item WHERE price < limit SELECT { id, name }?
}
fn main(): void {
rows := affordable(9.99) catch {
err => {
println("query failed")
return
}
}
println("${rows.len()} affordable items")
}Both declarations must be present in the same compilation unit as the query. See Models.
Modifiers
A declaration may carry visibility, safety, decorators, generic parameters,
and a where clause:
@inline
private fn largest[T](a: T, b: T): T
where T: Comparable[T]
{
if a > b { return a }
return b
}
pub const VERSION: string = "1.4.0"
pub struct Release { tag: string }
private struct Draft { note: string }
fn main(): void {
println("${VERSION} ${largest(2, 9)} ${Release { tag: VERSION }.tag}")
}| Modifier | Effect |
|---|---|
pub |
Reachable outside the declaring file; this is the default |
private |
Restricted to the declaring file |
unsafe |
Moves proof obligations onto the function and its callers |
@decorator |
Checked metadata for the compiler, runtime, or SQL layer |
unsafe
unsafe fn marks a declaration whose contract the compiler cannot verify —
raw addresses, hand-managed lifetimes, representation punning. The obligation
propagates: a caller must either be unsafe fn itself or open an
unsafe { ... } block, which is an expression like any other.
unsafe fn slot_offset(index: int): int => index * 8
unsafe fn two_slots(): int => slot_offset(1) + slot_offset(2)
fn main(): void {
near := unsafe { slot_offset(3) }
far := unsafe { two_slots() }
println("${near} ${far}")
}Calling one from ordinary code without that block is rejected, which is the whole point of the marker:
unsafe fn slot_offset(index: int): int => index * 8
fn main(): void {
println("${slot_offset(3)}")
}Keep the block as small as the obligation. Wrapping a whole function body in
unsafe { } hides which line actually needs the audit.
Decorators
Each decorator is interpreted by the subsystem that owns it. @derive
generates trait implementations on demand; @id marks a model’s primary key;
@inline and @weak are compiler hints.
@derive(Equatable, Hashable)
struct Tag {
name: string
}
fn unique(tags: []Tag): Set[Tag] {
mut seen := Set.new[Tag]()
for t in tags {
seen.add(t)
}
return seen
}
fn main(): void {
tags := [Tag { name: "a" }, Tag { name: "a" }, Tag { name: "b" }]
println("${unique(tags).size()}")
}@derive is demand-driven: it generates only the implementations something
actually asks for, so adding it to a type that is never compared or hashed
costs nothing. See Decorators.
Generic parameters
Type parameters are written in square brackets after the name. Bounds go
inline or in a where clause; both forms mean the same thing.
struct Page[T] {
items: []T
cursor: string?
}
fn first_of[T](p: Page[T]): T? => p.items.get(0)
fn largest[T: Comparable[T]](xs: []T): T? => xs.max()
fn pick[T](a: T, b: T): T where T: Comparable[T] {
if a > b { return a }
return b
}
fn main(): void {
p := Page { items: [3, 1, 4], cursor: None }
println("${first_of(p) ?? 0} ${largest(p.items) ?? 0} ${pick(2, 9)}")
}Each use site selects its own type arguments. See Generics.
Bodies
Ordinary functions need a block body or an => expression body. Type
declarations contain only member forms valid for that type.
fn double(value: int): int => value * 2
fn describe(value: int): string {
if value < 0 {
return "negative"
}
return "non-negative"
}
fn main(): void {
println("${double(21)} ${describe(-1)}")
}A signature without a body is a requirement, not an abstract runtime method.
It is legal in a trait, where an impl must supply it:
trait Store {
fn get(self, key: string): string?
fn put(mut self, key: string, value: string): void
}
struct Memo { entries: Map[string, string] }
impl Store for Memo {
fn get(self, key: string): string? => self.entries.get(key)
fn put(mut self, key: string, value: string): void {
self.entries.put(key, value)
}
}
fn main(): void {
mut m := Memo { entries: Map.new[string, string]() }
m.put("a", "1")
println(m.get("a") ?? "missing")
}Functions do not nest. A helper is a sibling declaration, not a declaration inside another body:
fn main(): void {
fn helper(): int => 1
println("${helper()}")
}Collection order
Top-level declarations are collected across the whole project before any body is resolved, so source order does not constrain reference order. A function may call one declared later, and a type may be used above its declaration.
fn main(): void {
println("${answer()} ${Config.default().retries}")
}
fn answer(): int => LUCKY_NUMBER
const LUCKY_NUMBER: int = 42
struct Config {
retries: int
fn default(): Config => Config { retries: MAX_RETRIES }
}
const MAX_RETRIES: int = 3This is declaration visibility, not hoisting of runtime effects. Local bindings inside a body still become visible only after their own statement:
fn main(): void {
println("${total}")
total := 10
}A useful file order — imports, public constants and types, public functions, private helpers — is a readability convention, not a resolution rule.
Namespaces
Declarations resolve in the namespace appropriate to the position they appear
in. A type name and a local binding can share spelling because they are never
looked up in the same place, while Type.member and a qualified enum variant
say explicitly which namespace to search.
enum Status { Open, Closed }
struct Ticket {
id: int
status: Status
fn is_open(self): bool => self.status == Status.Open
}
fn main(): void {
ticket := Ticket { id: 7, status: Open }
println("${ticket.id} ${ticket.is_open()}")
}Status.Open and the bare Open select the same variant; the qualified form
is worth writing when several enums in scope share a variant name.
Files and modules
Imports appear above the declarations and pull names in either individually or as a namespace alias.
import { mean, peak } from example.dev/telemetry/stats
import example.dev/telemetry/format as fmt
const HEADER: string = "daily report"
fn main(): void {
println(HEADER)
}A directory is a module: sibling files share one namespace and do not import
each other, so a file only ever imports from other directories. A file may
additionally open with a module example.dev/telemetry header naming the
module it belongs to; that name has to agree with the file’s location on disk,
so it is only meaningful inside a real project. See
Modules.
A complete module
The forms compose into an ordinary file: constants at the top, the types the API exposes, the trait that abstracts over them, and the functions callers reach for.
const WARN_CELSIUS: float = 80.0
const FAIL_CELSIUS: float = 100.0
error SensorError { Offline, Implausible }
enum Grade { Ok, Warn, Fault }
struct Sensor {
name: string
celsius: float
online: bool
}
trait Graded {
fn grade(self): Grade
fn label(self): string => "unlabelled"
}
impl Graded for Sensor {
fn grade(self): Grade {
if self.celsius >= FAIL_CELSIUS { return Fault }
if self.celsius >= WARN_CELSIUS { return Warn }
return Ok
}
fn label(self): string => self.name
}
fn read(s: Sensor): Grade ! SensorError {
if !s.online {
error Offline
}
if s.celsius < -273.15 {
error Implausible
}
return s.grade()
}
fn summarize[T](item: T): string where T: Graded {
match item.grade() {
Ok => return "${item.label()}: ok"
Warn => return "${item.label()}: warn"
Fault => return "${item.label()}: fault"
}
}
fn main(): void {
sensors := [
Sensor { name: "boiler", celsius: 91.5, online: true },
Sensor { name: "intake", celsius: 20.0, online: true },
Sensor { name: "flue", celsius: 140.0, online: false },
]
for s in sensors {
grade := read(s) catch {
err => {
println("${s.name}: unreadable")
continue
}
}
println(summarize(s))
}
}Evolution
A public declaration is a contract. These edits change source compatibility even when the body still computes the same answer:
| Change | Breaks |
|---|---|
| Rename or move a declaration | Imports and qualified references |
| Add a parameter without a default | Every call |
| Change a success or error type | Callers, ? propagation, and matches |
| Add an enum or error variant | Exhaustive matches |
| Add a trait member without a default | Every implementation |
| Change a field’s type or visibility | Construction, access, and layout |
| Add an effect to a signature | Callers with a narrower declared row |
Changing a parameter default is also API surface: existing calls keep compiling
and silently change behaviour. Keep helpers private so only the declarations
you intend to support are reachable from other files.