Skip to content

Source

Files, encoding, whitespace, comments, identifiers, statement boundaries, and block tails.

Updated View as Markdown

Atoll source files use the .at extension and UTF-8 text. A file is a list of declarations: constants, functions, structs, enums, errors, type aliases, traits, implementations, models, and schemas. Statements live inside function bodies, never at the top level.

const GREETING = "Hello"

fn greet(name: string): void {
    println("$GREETING, $name")
}

fn main(): void {
    greet("Ada")
}

A complete file

Files are conventionally ordered: module (optional), imports, constants, types, then functions. Nothing in the grammar enforces that order — forward references resolve fine — but it is what the standard library and the sample projects do.

module app.orders

const MAX_ITEMS = 100

/// An order placed by a customer.
struct Order {
    id: int
    total: float
}

/// Sum the totals of every order.
fn total_of(orders: []Order): float {
    mut sum := 0.0
    for order in orders {
        sum += order.total
    }
    return sum
}

fn main(): void {
    orders := [
        Order { id: 1, total: 9.99 },
        Order { id: 2, total: 20.01 },
    ]
    println("orders=${orders.len()} total=${total_of(orders)}")
}

Module identity normally comes from project metadata; a source-level module declaration is still accepted. Imports name a module path and either a brace list of items or an alias:

import { Result, Option } from std
import std.time as time

fn f(): void {
    println("imports parsed")
}

See Modules for project layout and visibility.

Top-level statements are illegal

A bare statement outside a function is ATOLL1019: Invalid statement. This is the single most common mistake when transcribing a snippet from prose.

counter := 0
counter += 1

Wrap it in a function instead:

fn main(): void {
    mut counter := 0
    counter += 1
    println("${counter}")
}

Functions do not nest, either. A fn inside a function body is a parse error; lift the helper to the top level, or use a closure.

fn outer(): int {
    fn inner(): int { return 1 }
    return inner()
}

Whitespace and layout

Spaces, tabs, and line breaks separate tokens. Indentation is conventional, not structural: braces delimit every block and declaration body. The style guide uses four spaces.

fn launch(): void {
    println("launched")
}

fn f(ready: bool): void {
    if ready {
        launch()
    }
}

Because layout is not syntax, a newline does not end a parenthesized argument list, collection literal, operator expression, or declaration body. Split long calls freely, and use a trailing comma:

fn calculate(subtotal: float, tax: float, discount: float): float {
    return subtotal + tax - discount
}

fn f(): float {
    return calculate(
        100.0,
        8.25,
        5.0,
    )
}

Statement boundaries

Newlines are trivia, not tokens. The parser ends a statement when the preceding form is complete and the next token cannot continue it. A semicolon is only needed to put two statements on one line, or to discard a block’s tail value.

fn compute(): int { return 7 }

fn f(): void {
    first := 1
    second := 2; third := 3
    println("${first} ${second} ${third} ${compute()}")
}

The fresh-line postfix rule

There is one deliberate exception to “newlines are trivia”: ( and [ do not continue a call or index when they start a fresh line. The lexer records whether a token had a leading newline, and a fresh-line ( begins a new expression instead of silently calling the previous line.

fn handler(x: int): int { return x }

fn f(): void {
    request := 1
    handler
    (request)
}

That program does not call handler(request). It evaluates handler (a function value), discards it, and then evaluates (request) — an int in a void function’s tail position, which is the reported type error. Keep the call attached:

fn handler(x: int): int { return x }

fn f(): void {
    request := 1
    println("${handler(request)}")
}

Dots and ? markers do continue a chain across lines, so a deliberately formatted postfix chain is fine as long as each continuation line begins with its marker:

struct Profile { name: string }
struct Client { }

fn Client.load(self, id: int): Profile? { return None }

fn f(client: Client): string? {
    return client
        .load(3)
        ?.name
}

Block tails

A block’s final expression, written without a semicolon, is the block’s value. Adding a semicolon discards it and makes the block void-valued.

fn compute(): int { return 7 }

fn f(): void {
    value := { compute() }        // int
    { compute(); }                // void — the value is discarded
    println("${value}")
}

Bindings, const, and defer are statements even without a semicolon; their initializers never become the block tail. Blocks and the rules for if/match values are covered in Blocks.

Comments

// begins a line comment. /* ... */ begins a block comment, and block comments nest.

// One line.

/*
    A block comment.
    /* Nested comments are valid. */
*/

fn value(): int {
    return 10 // trailing comment
}

/// and /** ... */ are documentation comments. The parser keeps them as trivia attached to the following declaration, which is what powers hover text and generated API docs.

/// Returns the larger input.
///
/// Ties resolve to `b`.
fn maximum(a: int, b: int): int {
    return if a > b { a } else { b }
}

/** Returns the smaller input. */
fn minimum(a: int, b: int): int {
    return if a < b { a } else { b }
}

Comments never affect runtime behavior — they are removed before parsing produces expressions.

Identifiers

Identifiers use ASCII letters, digits, and underscores, and cannot start with a digit. They are case-sensitive: Booking, booking, and BOOKING are three distinct names.

fn f(): void {
    retry_count := 0
    http2_enabled := true
    _unused := 3
    println("${retry_count} ${http2_enabled} ${_unused}")
}

The compiler’s naming lint expects:

  • snake_case for functions, methods, locals, parameters, and fields;
  • PascalCase for structs, enums, traits, errors, aliases, and variants;
  • SCREAMING_SNAKE_CASE for constants;
  • lowercase names for built-in scalar types (int, bool, string, byte).
const MAX_CONNECTIONS = 64

struct ConnectionPool {
    in_use: int
}

fn available(pool: ConnectionPool): int {
    return MAX_CONNECTIONS - pool.in_use
}

Naming is a lint, not a different grammar — a wrongly cased name still parses. Unicode is permitted inside string and character contents, but the identifier lexer is ASCII-only. The full table is in Naming.

Keywords

Structural words such as fn, struct, enum, trait, impl, if, match, for, return, break, continue, defer, spawn, and error are reserved. Query words such as FROM, WHERE, and SELECT are contextual and are recognized only inside an integrated query.

error deserves special mention: it is a keyword everywhere, so it can never be an identifier — not as a local, not as a parameter, not as a pattern binding.

fn f(): void {
    error := 1
    println("${error}")
}

Name error values err or e instead:

error LoadError { NotFound }

fn load(): int ! LoadError { error NotFound }

fn f(): int {
    match load() {
        Ok(v) => v
        Err(e) => -1
    }
}

Some, None, Ok, and Err are ordinary prelude declarations rather than literal keywords. They follow normal name resolution.

Some historical words remain lexer tokens even though the parser has no declaration for the feature they once named. A reserved token is not evidence that a design is implemented; check Feature Status.

Lexical and syntax errors

Lexing diagnoses unterminated strings and comments, malformed escapes, non-ASCII bytes in byte strings, and numeric literals the literal machinery cannot represent.

fn f(): string {
    return "oops
}

Parsing then diagnoses missing delimiters, misplaced modifiers, wrong binding separators, and forms outside the grammar. Recovery lets the compiler and the editor report more than one problem per file, but code generation requires a unit with no error diagnostics — warnings and hints do not block a build.

Putting it together

A small, complete, idiomatic file exercising most of this page:

module inventory

/// Maximum items a single request may reserve.
const MAX_RESERVATION = 10

/// One line of stock.
struct Item {
    sku: string
    on_hand: int
}

/*
    `reserve` clamps the request to what is on hand and to MAX_RESERVATION.
    /* Nested comment: the clamp order matters when on_hand is small. */
*/
fn reserve(item: Item, requested: int): int {
    limit := if requested > MAX_RESERVATION { MAX_RESERVATION } else { requested }
    return if limit > item.on_hand { item.on_hand } else { limit }
}

fn main(): void {
    items := [
        Item { sku: "A-1", on_hand: 3 },
        Item { sku: "B-2", on_hand: 40 },
    ]
    for item in items {
        println("${item.sku}: reserved ${reserve(item, 12)}")
    }
}
Navigation

Type to search…

↑↓ navigate↵ selectEsc close