Skip to content

Diagnostics

Read real Atoll compiler output — the diagnostic families you hit first, the code that produces each one, and the fix.

Updated View as Markdown

Every diagnostic on this page is real output, produced by running the compiler on the source shown next to it. The shape is always the same:

severity[CODE]: message at file:line:column

followed by a summary line — N error(s) found when the compilation failed, or OK (N diagnostics) when only warnings and hints were emitted. Warnings and hints never stop a build.

Two commands, two gates

atoll check resolves names, types, effects, and errors. atoll build also lowers the program to WebAssembly. A program can pass check and fail build, so treat check as a fast inner loop and build as the real acceptance test:

fn banner(titles: []string): string {
    return titles.join(", ")
}

fn main(): void {
    println(banner(["ship", "review"]))
}
$ atoll check src/main.at
OK (0 diagnostics)

$ atoll build src/main.at
error[ATOLL2004]: builtin method `join` has no lowering path — declared in the
prelude but never implemented (no body, host import, or intrinsic), so this
call cannot be compiled to wasm at src/main.at:2:12
1 error(s); build aborted

ATOLL2004 means the method exists in the prelude — so it type-checks — but has no implementation behind it. Only reachable code is lowered, so an unused function containing such a call will never report it. Write the loop yourself until the method lands:

fn banner(titles: []string): string {
    mut out := ""
    for title in titles {
        if !out.is_empty() { out += ", " }
        out += title
    }
    return out
}

fn main(): void {
    println(banner(["ship", "review"]))
}

Feature Status lists which prelude methods are currently in this state.

Unresolved names — ATOLL1009

The resolver runs before typing, so a misspelled name is reported on its own and nothing downstream of it is trustworthy.

struct WorkItem {
    title: string
    done: bool
}

fn open_titles(items: []WorkItem): []string {
    mut out: []string = []
    for item in items {
        if !item.done {
            out.add(itm.title)
        }
    }
    return out
}
$ atoll check src/main.at
error[ATOLL1009]: unresolved name `itm` at src/main.at:10:21
1 error(s) found

The range points at the use, not at a declaration, because the compiler has no candidate to point to. The same code covers a name that exists but is not in scope: a private declaration in another file, or a member missing from an import list.

struct WorkItem {
    title: string
    done: bool
}

fn open_titles(items: []WorkItem): []string {
    mut out: []string = []
    for item in items {
        if !item.done {
            out.add(item.title)
        }
    }
    return out
}

fn main(): void {
    mut items: []WorkItem = []
    items.add(WorkItem { title: "ship", done: false })
    items.add(WorkItem { title: "review", done: true })
    println("${open_titles(items).len()} open")
}

A near miss on a method is a different code, because the receiver’s type is already known:

fn shout(word: string): string {
    return word.uppercase()
}
$ atoll check src/main.at
error[ATOLL2003]: no method `uppercase` on type `string` at src/main.at:2:12
1 error(s) found

Type mismatches — ATOLL2002

A mismatch names both types and, where the checker recorded one, the boundary that established the expectation.

struct WorkItem {
    title: string
    weight: string
}

fn total_weight(items: []WorkItem): int {
    mut total: int = 0
    for item in items {
        total = item.weight
    }
    return total
}
$ atoll check src/main.at
error[ATOLL2002]: expected `int`, found `string` at src/main.at:9:17
1 error(s) found

The parenthetical suffix is the blame: (from a type annotation), (in argument position), (operands of a binary operator). It tells you which side is the expectation, which is usually what you actually want to change.

The mismatch you will hit first is Option. Indexing and get return T?, never T:

fn first_plus_one(xs: []int): int {
    return xs[0] + 1
}
$ atoll check src/main.at
error[ATOLL2002]: type mismatch: expected `int?`, got `int` (operands of a
binary operator) at src/main.at:2:12
error[ATOLL2002]: expected `int`, found `int?` at src/main.at:2:12
2 error(s) found

Two diagnostics, one cause — the second is the constraint the first one violated. Fix the earliest one and both go away:

fn first_plus_one(xs: []int): int {
    return (xs[0] ?? 0) + 1
}

fn main(): void {
    println("${first_plus_one([41])}")
}

Related codes on the same axis: ATOLL3010 for an unknown field and ATOLL3012 for a call arity error.

fn rescale(value: int, factor: int): int {
    return value * factor
}

fn main(): void {
    println("${rescale(3)}")
}
$ atoll check src/main.at
error[ATOLL3012]: function `rescale` takes 2 arguments, but 1 was supplied
at src/main.at:6:16
1 error(s) found

Non-exhaustive match — ATOLL2010

The message names the type and lists the variants you left out.

enum Priority {
    Low
    Normal
    Urgent
}

fn marker(p: Priority): string {
    match p {
        Low => "."
        Normal => "-"
    }
}
$ atoll check src/main.at
error[ATOLL2010]: non-exhaustive `match` on `Priority` — missing: Urgent
at src/main.at:8:5
1 error(s) found

This is the diagnostic that turns adding an enum variant into a checklist. Add the arm rather than a _, unless treating the remainder uniformly is a real decision:

enum Priority {
    Low
    Normal
    Urgent
}

fn marker(p: Priority): string {
    match p {
        Low => "."
        Normal => "-"
        Urgent => "!"
    }
}

fn main(): void {
    println(marker(Priority.Urgent))
}

The same code and the same rule govern catch arms, which are match arms over the error channel.

? outside a fallible function — ATOLL3051

? returns through an error channel. A function without one cannot host it.

error WorkError { EmptyTitle }

fn normalize(title: string): string ! WorkError {
    clean := title.trim()
    if clean.is_empty() { error EmptyTitle }
    return clean
}

fn main(): void {
    println(normalize("  ship  ")?)
}
$ atoll check src/main.at
error[ATOLL3051]: function `main` uses `error`/`?` but its return type `()` is
not fallible — declare `() ! <Error>` or `Result[(), <Error>]`
at src/main.at:9:4
error[ATOLL2002]: ? on result requires an enclosing fallible function
at src/main.at:10:13
warning[ATOLL2032]: fn `main` inherits effect `error` from a
transitively-called callee but its declared `using [..]` row does not list
`error` at src/main.at:10:13
2 error(s) found

Three lines, one cause: ATOLL3051 at the declaration that lacks the error channel, ATOLL2002 at the ? itself, and an effect warning riding along. Read the declaration-level one first.

main is usually the place where propagation should stop and a decision should be made, so handle the Result instead of widening main:

error WorkError { EmptyTitle }

fn normalize(title: string): string ! WorkError {
    clean := title.trim()
    if clean.is_empty() { error EmptyTitle }
    return clean
}

fn main(): void {
    match normalize("  ship  ") {
        Ok(clean) => println("accepted: ${clean}")
        Err(EmptyTitle) => println("rejected: empty title")
    }
}

That program still emits one ATOLL2032 warning, because main calls a fallible function and therefore inherits the error effect without declaring it. It compiles — see below.

Effect rows — ATOLL2032 and ATOLL2031

The compiler infers what a body actually needs and compares it against the declared using [...] row.

fn load_title(id: int): string {
    return "item ${id}"
}

fn describe(id: int): string {
    return load_title(id)
}

fn main(): void {
    println(describe(7))
}
$ atoll check src/main.at
hint[ATOLL2031]: fn `main` inherits effect `suspend` from a
transitively-called callee at src/main.at:10:13
warning[ATOLL2032]: fn `describe` inherits effect `suspend` from a
transitively-called callee but its declared `using [..]` row does not list
`suspend` at src/main.at:6:12
OK (2 diagnostics)

Read the two apart:

  • ATOLL2032 (warning) — the function’s declared effect row is narrower than the effects it actually inherits. A function with no using clause declares nothing, so this fires for essentially every effectful function.
  • ATOLL2031 (hint) — informational. It fires even when the row is correct, recording where an inherited effect entered.

Neither stops the build, and for ordinary code the right response to both is to ignore them. Effects are inferred; you are not expected to annotate. Real Atoll code carries no using clauses at all — there is not one in the compiler’s sample projects or prelude. Reach for the row only when you are deliberately pinning a contract, and see Effects for that case.

Treat ATOLL2032 as a signal worth reading only when you expected a function to stay effect-free — then it tells you purity slipped and where:

fn load_title(id: int): string {
    return "item ${id}"
}

fn describe(id: int): string {
    return load_title(id)
}

fn main(): void {
    println(describe(7))
}

Widen a row when callers genuinely should permit the capability; narrow the call when they should not. The effects are Suspend, Error, Alloc, Spawn, and Cancel — see Effects.

Missing trait implementations — ATOLL3100

String interpolation goes through Display, so a bare ${item} on a struct is a missing-impl error rather than a fallback to some debug format.

struct WorkItem {
    title: string
    done: bool
}

fn main(): void {
    item := WorkItem { title: "ship", done: false }
    println("${item}")
}
$ atoll check src/main.at
error[ATOLL3100]: type `WorkItem` does not implement `Display` (required for
`${...}` segments in string templates) at src/main.at:8:13
1 error(s) found

Either interpolate a field, or implement the trait:

struct WorkItem {
    title: string
    done: bool
}

impl Display for WorkItem {
    fn to_string(self): string {
        mark := if self.done { "x" } else { " " }
        return "[${mark}] ${self.title}"
    }
}

fn main(): void {
    item := WorkItem { title: "ship", done: false }
    println("${item}")
}

Syntax cascades — ATOLL1xxx

The parser inserts recovery nodes so one bad construct does not hide the rest of the file. The cost is that a single mistake can produce a spray of diagnostics, several of them nonsense.

struct WorkItem {
    title: string
    done: bool
}

fn main(): void {
    item := WorkItem {
        title: "Publish guide"
        done: false
    }
    println(item.title)
}
$ atoll check src/main.at
error[ATOLL1006]: Expected '}' at src/main.at:9:9
error[ATOLL1022]: Expected type expression at src/main.at:9:15
error[ATOLL1019]: Invalid statement at src/main.at:11:5
error[ATOLL2002]: type mismatch: expected `void`, got `bool` (in argument
position) at src/main.at:9:15
warning[ATOLL4100]: local binding `item` is declared but never used
at src/main.at:7:5
warning[ATOLL4100]: local binding `done` is declared but never used
at src/main.at:9:9
4 error(s) found

Six diagnostics from one missing comma after "Publish guide". Struct declarations separate fields by newline; struct literals require commas.

The rule for any noisy run: fix the first syntax error in each file and rerun before reading anything else. Recovery is not acceptance — a placeholder node never becomes a runtime value.

Warnings and lints

Two warnings show up constantly and neither fails a build.

ATOLL4100 flags a binding you never read:

fn main(): void {
    scratch := "ship".len()
    println("done")
}
$ atoll check src/main.at
warning[ATOLL4100]: local binding `scratch` is declared but never used
at src/main.at:2:5
OK (1 diagnostics)

ATOLL7002 is the naming lint. It is opt-in — set ATOLL_LINT_NAMING=1:

struct workItem {
    Title: string
}

fn MakeItem(name: string): workItem {
    return workItem { Title: name }
}

fn main(): void {
    println(MakeItem("ship").Title)
}
$ ATOLL_LINT_NAMING=1 atoll check src/main.at
warning[ATOLL7002]: struct name `workItem` should be `WorkItem` (Atoll uses
PascalCase for structs) at src/main.at:1:8
warning[ATOLL7002]: function name `MakeItem` should be `make_item` (Atoll uses
snake_case for functions) at src/main.at:5:4
OK (2 diagnostics)

@allow(naming) on a declaration suppresses it for that declaration:

@allow(naming)
struct workItem {
    Title: string
}

fn main(): void {
    println(workItem { Title: "ship" }.Title)
}

Suppression is per-declaration and only applies to compiler-recognized lints. Generic decorator syntax does not invent new warning categories, and nothing suppresses a type error, an effect error, or ATOLL2004.

Code families

The leading digits narrow the stage, which narrows the fix:

Range Stage Example
1xxx Lexing, parsing, resolution ATOLL1006 expected }, ATOLL1009 unresolved name
2xxx Typing, exhaustiveness, effects, lowering availability ATOLL2002 mismatch, ATOLL2010 non-exhaustive, ATOLL2004 no lowering path
3xxx Members, calls, fallibility, trait obligations ATOLL3010 unknown field, ATOLL3012 arity, ATOLL3051 ? in a non-fallible fn, ATOLL3100 missing impl
4xxx Dead and unused code ATOLL4100 unused local
5xxx, 6xxx Backend and monomorphization ATOLL6002 structural type-arity mismatch
7xxx Style and advisory lints ATOLL7002 naming

A 5xxx or 6xxx code means checked source hit a representation edge, and it only ever comes from atoll build. The common one is ATOLL6002, which usually means you passed a builtin a closure of the wrong arity — check the signature in crates/atoll-sema/src/stubs/. Anything else in that range is worth reporting as a compiler bug.

Triage order

  1. Fix the first syntax error in each file. Rerun.
  2. Fix unresolved names — everything after them is a guess.
  3. Fix the first type error in each constraint chain, reading the blame suffix to decide which side is wrong.
  4. Satisfy trait and exhaustiveness obligations.
  5. Decide error and effect policy: who propagates, who handles, which rows widen.
  6. Only then run atoll build and address any lowering failure.

Working the list out of order wastes time on diagnostics that will disappear on their own.

Reporting a compiler bug

Include:

  • the smallest source that reproduces it, in current syntax;
  • whether check or build failed, and the full diagnostic text with code and range;
  • the atoll.toml fields that matter (module, dependencies, host access, SQL datasources) when the failure is project-scoped;
  • compiler revision and target.

Preserve the first unexpected diagnostic and delete unrelated code around it. A ATOLL2004 on a prelude method is already a complete report — the method name is the bug.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close