Skip to content

Feature Status

What actually compiles — the gap between type-checking and lowering, which prelude methods are still declaration-only, and how to test any API yourself.

Updated View as Markdown

Atoll’s prelude declares more than the backend implements. That is not a secret failure mode — the compiler reports it precisely — but it does mean atoll check passing is not the same as your program compiling. This page tells you where the line currently sits and how to test any API yourself.

The two gates

atoll check resolves names, types, effects, and errors. atoll build does all of that and then lowers to WebAssembly. Only reachable code is lowered, so the second gate is the one that finds a missing implementation:

fn shout(word: string): string {
    return word.to_upper_case()
}

fn main(): void {
    println(shout("ship"))
}
$ atoll check src/main.at
OK (0 diagnostics)

$ atoll build src/main.at
error[ATOLL2004]: builtin method `to_upper_case` 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

Two consequences follow, and both matter in practice:

  • An unimplemented method inside a function nobody calls will never be reported. Dead code is not lowered.
  • A program that checks clean can still stop at build. Run atoll build before you believe an API works.

Test any API in six lines

The fastest way to settle a question about the prelude is to call the method from main and build:

fn main(): void {
    xs := ["ship", "review"]
    println(xs.join(", "))
}
$ 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:3:16
1 error(s); build aborted

Three outcomes are possible, and each tells you something different:

Result Meaning
ATOLL2003: no method X on type Y from check Not in the prelude at all — you have the wrong name or the wrong receiver
ATOLL2004 from build Declared in the prelude, no implementation behind it
ATOLL6002 from build Implemented, but you called it with the wrong argument or closure shape
Wrote 1 unit(s) Implemented end to end

The prelude stubs themselves are readable source: crates/atoll-sema/src/stubs/*.at. A method declared there without a body is a candidate for ATOLL2004 — but not a certainty, because many bodyless declarations are backed by an intrinsic or a host import (Map.get, List.to_string, string.split all work).

Declared but not implementable

The lists below were produced by calling each method from main and running atoll build. Every entry type-checks; none of them lowers. Do not use them in code you intend to compile.

string

The string surface is the largest gap. Not implemented:

to_upper_case, to_lower_case, capitalize, title_case, replace, replace_first, reversed, pad_start, pad_end, take, take_last, drop, drop_last, first, last, is_blank, count, equals_ignore_case, compare_to_ignore_case, split_with_limit, lines, words, chars, enumerate, map, filter, any, all, none, fold, to_bool, to_list.

Implemented and safe to use: len, length, is_empty, is_not_empty, trim, trim_start, trim_end, contains, starts_with, ends_with, index_of, last_index_of, split, substring, repeat, char_at, to_upper_ascii, to_lower_ascii, to_int, to_float, to_bytes, byte_length, to_string.

Note that to_upper_ascii and to_lower_ascii are the working case conversions. string has no size() — use len().

List[T]

Not implemented: join, join_of, chunked, windowed, group_by, associate, zip, flatten, distinct_by, shuffled.

Everything else in the list surface lowers, including the parts people reach for most: map, filter, fold, reduce, scan, flat_map, any, all, none, find, find_index, count, sorted, sorted_by, sorted_descending, sorted_by_descending, distinct, take, skip, take_while, skip_while, reversed, partition, associate_by, zip_with_index, plus, plus_all, union, intersect, subtract, to_set, min, max, sum, average, min_of, max_of, sum_of, min_by, max_by, slice, clone, capacity, to_string.

Map[K, V] and Set[T]

Both hash types are implemented broadly — lookup, mutation, iteration, filter/map/fold families, and the set algebra all lower.

SortedMap[K, V] and SortedSet[T] are a different matter: they are declared types whose entire method surface is unimplemented, and the ordered accessors that would return them (Map.first_key, Map.floor_key, Map.head_map, Set.first, Set.floor, Set.head_set, to_sorted_map, to_sorted_set) are not reachable from Map/Set at all — those calls fail at check with ATOLL2003. Treat ordered collections as absent.

The other build-only failure: ATOLL6002

A second class of error also survives check and dies at build: passing a builtin a closure of the wrong shape. List.sorted_by takes a key function fn(T) -> K, not a comparator, and inference does not catch the difference:

fn main(): void {
    xs := [3, 1, 4]
    println("${xs.sorted_by((a, b) => a - b).len()}")
}
$ atoll check src/main.at
OK (0 diagnostics)

$ atoll build src/main.at
error[ATOLL6002]: monomorphization detected a structural type-arity mismatch
while specializing `sorted_by` — sema may have missed an inference shape error
at src/main.at:1:1
1 error(s); build aborted

The diagnostic names the method, which is the useful part; the source range is the file, not the call. When you see ATOLL6002, read the stub signature for the named method and check the arity of every closure and argument you passed. The correct call is xs.sorted_by(v => v). List.reduce is reduce(initial, fn(R, T) -> R), Map.map_values takes fn(K, V) -> V2, and List.slice takes a range — xs.slice(0..2), not xs.slice(0, 2).

The message says “sema may have missed an inference shape error”, and that is exactly what happened: this should be a check-time diagnostic and is not yet.

Working around a gap

Every gap above has a loop-shaped workaround, and the loop compiles:

fn join_with(parts: []string, sep: string): string {
    mut out := ""
    for part in parts {
        if !out.is_empty() { out += sep }
        out += part
    }
    return out
}

fn chunk(xs: []int, size: int): [][]int {
    mut out: [][]int = []
    mut current: []int = []
    for x in xs {
        current.add(x)
        if current.len() == size {
            out.add(current)
            current = []
        }
    }
    if current.is_not_empty() { out.add(current) }
    return out
}

fn main(): void {
    println(join_with(["ship", "review"], " / "))
    println("SHOUT".to_lower_ascii())
    for group in chunk([1, 2, 3, 4, 5], 2) {
        println("${group.len()}")
    }
}

When you hit a gap, prefer a local helper over restructuring your data. The missing methods are missing implementations, not missing designs — their signatures are stable.

Language constructs

The language itself, as distinct from the library, is in much better shape. Every construct the guide documents lowers and runs. A single program exercising declarations, generics, traits, errors, and collections:

enum Priority { Normal  Urgent }

error WorkError { EmptyTitle }

struct WorkItem {
    title: string
    priority: Priority
    done: bool
}

trait Summary {
    fn summary(self): string
}

impl Summary for WorkItem {
    fn summary(self): string {
        marker := match self.priority {
            Normal => "-"
            Urgent => "!"
        }
        return "${marker} ${self.title}"
    }
}

fn best[T: Comparable[T]](values: []T, fallback: T): T {
    mut winner := fallback
    for v in values {
        if winner < v { winner = v }
    }
    return winner
}

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

fn main(): void {
    match normalize("  Publish guide  ") {
        Ok(title) => {
            item := WorkItem { title: title, priority: Urgent, done: false }
            println(item.summary())
        }
        Err(EmptyTitle) => println("empty title")
    }
    println("${best([3, 9, 4], 0)}")
}

Concurrency lowers too — spawn, Task.await, select, race, and Stream[T] all reach WebAssembly:

fn produce(s: Stream[int]): void {
    mut i := 0
    for i < 3 {
        s.send(i)
        i += 1
    }
    s.close()
}

fn main(): void {
    s := Stream.new(4)
    worker := spawn { produce(s) }
    mut total := 0
    for v in s {
        total += v
    }
    worker.await()
    println("total ${total}")
}

So does integrated SQL, given a schema and model in the same unit:

schema shop

model Item {
    @id
    id: int
    name: string
    price: float
}

fn cheap(limit: float): []Item ! QueryError {
    return FROM Item WHERE price < limit?
}

fn main(): void {
    match cheap(10.0) {
        Ok(rows) => println("${rows.len()} rows")
        Err(e) => println("query failed")
    }
}

Two constructs to know about, because they parse without doing what you might expect:

  • Decorators. The syntax @name(args) parses on any declaration, but only compiler-recognized names have meaning (@id on a model field, @allow(naming) on a declaration). An unrecognized decorator is not an extension point.
  • The naming lint. ATOLL7002 exists but is opt-in behind ATOLL_LINT_NAMING=1. Style is not enforced by default.

Historical declarations

Several legacy design documents describe an application framework that the current declaration parser does not accept. If you find one of these in an old document or an LLM’s suggestion, it is not current syntax:

Legacy concept Current status
actor, actor messages, supervision No actor declaration; use structs, tasks, and streams
service declarations, implicit wiring No service declaration; use modules and functions
topic / messages declarations Use Stream[T] or a host API
job / workflow declarations Scheduling and orchestration belong to a host layer
Implicit context parameters No implicit threading rule; pass a parameter
UI, auth, AI, MCP, route, event decorator catalogs Generic decorator syntax parses; the behavior is not in the compiler
Model inheritance and behaviors Superseded by the current SQL model declaration
Logger facade, automatic error logging No logger prelude; println and host integrations are separate

Likewise, several docs/builtin/ files describe library types that are not installed: Any (runtime type erasure), Email, Phone, Url, Money, Queue, Stack. Nothing prevents a library from defining these; their absence from the prelude means their behavior is not a language contract.

What “verified” means on a page

Each page in this guide carries a status field:

  • verified — every code block on the page passes atoll check, and every block with an entry point also passes atoll build. The book’s example gate enforces this.
  • provisional — the core is implemented but the public contract is still moving; individual claims are backed by narrower evidence.
  • historical — preserved for context; does not define current behavior.

A block marked ```atoll fail in the source is one the compiler must reject; the gate fails if such a block starts compiling. That is how the guide’s error examples stay honest as diagnostics change.

If you find a documented API that does not build, that is a book bug and a compiler gap at once — report both. See Diagnostics for what to include.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close