Skip to content

Decorators

Attach checked compiler, tooling, runtime, and data metadata to source declarations.

Updated View as Markdown

A decorator begins with @ and precedes the declaration, field, parameter, or binding it modifies. It never changes what an expression evaluates to — it tells one compiler pass, tool, or subsystem something extra about the target.

@inline
fn small(value: int): int {
    return value + 1
}

@sealed
trait Protocol {
    fn execute(self): int
}

struct Runner {
    @private secret: int
    id: int
}

impl Protocol for Runner {
    fn execute(self): int { return self.id + self.secret }
}

fn main(): void {
    r := Runner { secret: 5, id: 7 }
    println("${small(r.execute())}")   // 13
}

The parser accepts arbitrary decorator syntax. A decorator has behavior only when a compiler pass, tool, runtime adapter, or domain subsystem recognizes it by name.

Syntax

A decorator name is lowercase and may be dotted for a subsystem namespace. Arguments are parsed as ordinary expressions, but each recognized decorator imposes its own narrower contract on them. Written without parentheses, a decorator has no arguments.

Several decorators can stack on one target, and their source order does not change their meaning:

@host("clock", "monotonic")
@suspend
fn monotonic_time(): int

@host("clock", "coarse")
@pure
fn coarse_time(): int

fn main(): void {
    start := coarse_time()
    now := monotonic_time()
    println("${now - start}")
}

A decorator attaches to the immediately following supported target. It does not float across an unrelated declaration, and one left with no target is a syntax error rather than file-level metadata:

@inline
fn orphan(): int { return 1 }

@inline

Decorator arguments are compile-time metadata, not runtime expressions. The recognizing consumer decides which shapes it accepts and diagnoses the rest — @derive, for instance, insists on bare trait names:

@derive("Equatable")
struct Coordinate {
    x: int
    y: int
}

Compilation

These decorators are read during ordinary compilation:

Decorator Target Meaning
@private Field Restrict reads to methods on the owning type
@allow(naming) Supported declaration Suppress its naming-lint warning
@sealed Trait Restrict implementations to the declaring file
@inline Function Raise the inliner’s threshold for this callee
@inline(always) Function Inline regardless of size
@noinline Function Never inline
@linear Local binding Give the local single-owner (move, not retain) RC mode
@weak Field Store an optional reference without an owning count
@externalizable Struct or enum Force serialization codegen
@extern Struct or enum Alias of @externalizable
@cycle_free Struct Assert the type can never take part in an RC cycle
@derive(Trait, ...) Struct or enum Parsed and validated; no synthesizer consumes it yet

Field privacy

@private is the one decorator on this list that changes whether a program is accepted. A private field is readable from methods declared on its own type:

struct Account {
    @private cents: int
    holder: string
}

impl Account {
    fn open(holder: string): Account {
        return Account { cents: 0, holder: holder }
    }

    fn deposit(mut self, amount: int): void {
        self.cents += amount
    }

    fn balance(self): int { return self.cents }
}

fn main(): void {
    mut a := Account.open("ada")
    a.deposit(250)
    println("${a.holder} ${a.balance()}")   // ada 250
}

Reaching for it from outside is ATOLL3030:

struct Account {
    @private cents: int
}

fn main(): void {
    a := Account { cents: 100 }
    println("${a.cents}")
}

Inlining

@inline, @inline(always), and @noinline steer one per-callee decision in the optimizer. They are pure hints: removing all three must not change what the program computes.

@inline(always)
fn clamp(value: int, low: int, high: int): int {
    if value < low { return low }
    if value > high { return high }
    return value
}

@noinline
fn report(label: string, value: int): void {
    println("${label}=${value}")
}

fn main(): void {
    for raw in [-5, 3, 99] {
        report("clamped", clamp(raw, 0, 10))
    }
}

Sealing a trait

@sealed restricts implementations of a trait to the file that declares it, so the set of implementing types is closed and reviewable. An impl in the same file is fine; a cross-file one raises ATOLL2106.

@sealed
trait Codec {
    fn tag(self): int
}

struct Json { version: int }
struct Binary { version: int }

impl Codec for Json {
    fn tag(self): int { return 1 }
}

impl Codec for Binary {
    fn tag(self): int { return 2 }
}

fn main(): void {
    println("${Json { version: 1 }.tag()} ${Binary { version: 1 }.tag()}")
}

Weak fields

@weak marks an optional reference field that does not keep its target alive — the classic child-to-parent back pointer. The compiler requires the type to also carry a strong edge, otherwise the graph has no owner at all and it reports ATOLL3222:

struct Node {
    value: int
    children: []Node
    @weak parent: Node?
}

fn main(): void {
    leaf := Node { value: 2, children: [], parent: None }
    root := Node { value: 1, children: [leaf], parent: None }
    println("${root.value} ${root.children.len()} ${root.parent?.value ?? -1}")
}

See References for the layout rules — @weak applies to optional reference-shaped fields, not to scalars, strings, or tuples.

Naming lint

The naming lint is opt-in; it runs when the compiler is invoked with ATOLL_LINT_NAMING=1. @allow(naming) suppresses the warning for one declaration, which is what you want for a name that mirrors an external protocol.

@allow(naming)
fn XMLPayload(): int { return 2 }

fn main(): void {
    println("${XMLPayload()}")
}

Suppression is retained on functions, structs, enums, error types, traits, error unions, and implementation blocks; enum-level suppression also covers variants. Aliases and constants do not currently retain it. See Naming conventions.

Derive, honestly

@derive(Trait, ...) parses, validates that every argument is a bare trait name, and stores the list — but no pass currently reads it, so it synthesizes nothing today. Write it as forward-looking documentation if you like; do not depend on it.

You rarely need it. Structural equality, ordering, and hashing already work on any struct whose fields support them, with no declaration at all:

struct Coordinate {
    x: int
    y: int
}

fn main(): void {
    a := Coordinate { x: 1, y: 2 }
    b := Coordinate { x: 1, y: 2 }

    println("${a == b} ${a < b}")

    mut seen := Set.new[Coordinate]()
    seen.add(a)
    seen.add(b)
    println("${seen.size()}")   // 1 — structurally equal, one entry
}

What that synthesis does not cover is a generic bound such as T: Comparable[T]. When a generic function demands one, write the impl — see Operators.

Tooling

The CLI recognizes three entry-point decorators. They take no arguments.

Decorator Meaning
@test atoll test discovers and runs the function
@bench atoll bench runs the function repeatedly and reports timings
@ignore Skip a discovered test unless --include-ignored is passed

A test is an ordinary void function; there is no assert in the prelude, so report failures the way any other Atoll code reports something — print, or return early. Nothing prints when a test passes.

fn checksum(values: []int): int {
    mut total := 0
    for v in values { total = total * 31 + v }
    return total
}

@test
fn checksum_is_order_sensitive(): void {
    forward := checksum([1, 2, 3])
    backward := checksum([3, 2, 1])
    if forward == backward {
        println("FAIL: checksum ignored element order")
    }
}

@test
@ignore
fn checksum_large_input(): void {
    mut values: []int = []
    for i in 0..100000 { values.add(i) }
    println("${checksum(values)}")
}

fn main(): void {
    println("${checksum([1, 2, 3])}")
}

atoll test on that unit prints:

test checksum_is_order_sensitive ... ok
test checksum_large_input ... ignored

result: 1 ok, 0 failed, 1 ignored

@bench mirrors @test but reports min/mean/max over repeated iterations, so keep the body free of output:

fn checksum(values: []int): int {
    mut total := 0
    for v in values { total = total * 31 + v }
    return total
}

@bench
fn checksum_bench(): void {
    mut values: []int = []
    for i in 0..256 { values.add(i) }
    _ := checksum(values)
}

fn main(): void {
    println("${checksum([1, 2])}")
}

Runtime

The prelude and the runtime adapter use a lower-level set that defines the compiler/runtime ABI:

Decorator Meaning
@host("namespace", "name") Bind a body-less function to a typed wasm import
@pure The host binding is synchronous and does not suspend
@suspend The function may yield to the runtime event loop
@builtin The implementation is supplied by compiler lowering
@intrinsic Bind a function to its same-named intrinsic
@intrinsic("name") Bind to an explicitly named intrinsic
@niche Select a compiler-known niche (arena-handle) representation
@cycle_free Assert that a type cannot form RC cycles

@host requires exactly two string arguments and the (namespace, name) pair must be unique across the project. @builtin, @pure, @suspend, @niche, and @cycle_free take no arguments.

The combination rules are enforced, not conventional:

@builtin
@host("clock", "now")
fn broken(): int
@pure
fn not_a_host(): int { return 1 }
@host("clock", "now")
@pure
@suspend
fn conflicted(): int

A @suspend binding makes every transitive caller suspending, which shows up in effect rows; a @pure @host binding does not.

The removed @kind routing decorator produces a migration diagnostic (ATOLL2025) pointing at the two-argument @host form:

@kind(3)
fn legacy(): int

Application code should call safe standard-library functions rather than declare new host imports or intrinsics. @cycle_free is an assertion you owe the compiler a proof for — if a cycle forms anyway, it leaks:

@cycle_free
struct Label {
    text: string
}

fn main(): void {
    l := Label { text: "release-1" }
    println(l.text)
}

Data

The data layer recognizes @id, @unique, @sql.name, @sql.function, full-text metadata, vector dimensions, and index options. Models live in a unit that also declares a schema:

schema directory

@sql.name("people")
model Person {
    @id
    id: int

    @unique
    email: string

    display_name: string
}

fn main(): void {
    p := Person { id: 1, email: "[email protected]", display_name: "Ada" }
    println("${p.id} ${p.email} ${p.display_name}")
}

Their valid targets, arguments, and engine restrictions are documented in Models and Schemas.

Unknown names

Core parsing retains a decorator it does not recognize, so domain-specific tools can inspect it. Sema otherwise ignores the name and does not check the arguments:

@acme.reviewed("2026-07")
fn calculate(): int => 42

@totally.unknown(1, "two", [3])
struct Widget {
    id: int
}

fn main(): void {
    println("${calculate()} ${Widget { id: 1 }.id}")
}

Preservation does not mean the decorator executes, validates its arguments, changes code generation, or exists at runtime. A subsystem has to implement those semantics.

Review

When a decorator changes correctness rather than optimization, document the contract beside the target:

Kind Review question
Representation Which layout or ABI invariant does the compiler assume?
Host binding Which capability, operation name, error, and suspension contract applies?
Visibility/ownership Which source boundary or lifetime rule changes?
Tooling Does omitting the tool still leave valid program semantics?
Data Which schema, migration, or dialect behavior changes?

Optimization hints such as @inline must not change program meaning. Assertion-style decorators such as @cycle_free, and low-level ABI markers such as @niche or @intrinsic, carry proof obligations and should stay in compiler/prelude code unless their subsystem explicitly supports application use.

The legacy docs/decorators/ catalog proposes actor, job, UI, auth, AI, MCP, HTTP-routing, and event decorators. Those application systems are not current core-language features and remain classified under Feature Status.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close