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 }
@inlineDecorator 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(): intA @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(): intApplication 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.