Annotations reference
Every @… form the parser recognises, what it attaches to, and
how the compiler reacts. There are two distinct annotation
flavours :
- Loop annotations — sit immediately above a
for/while/do-whileloop. They influence how LLVM vectorises / unrolls the loop body. The parser rejects them anywhere else. - Declaration annotations — sit immediately above a declaration,
a method or a field. Each applies to some of those only; the compiler
refuses an unknown name (
E0011, suggesting the closest recognised one when the misspelling is close enough to guess) and a known one where it does not apply (E0012).
What this page covers: the four loop annotations · the declaration
annotations one by one — @link, the @float / @heat / @layout axes, @derive · a table of where each declaration annotation is accepted · what
happens to an unknown or misplaced one · the values each annotation will and
will not take.
Loop annotations
@vectorize
fn addInto(out : i32[], a : i32[], b : i32[], n : i32) {
@vectorize
for i of 0..n { out[i] = a[i] + b[i]; }
} Sets llvm.loop.vectorize.enable = true on the loop. LLVM picks
the vector width based on the host’s SIMD level. See SIMD and auto-vectorisation.
@vectorize(disable)
fn fold(table : i32[], n : i32) : i32 {
let state : i32 = 0;
@vectorize(disable)
for i of 0..n { state = state * 31 + table[i]; }
return state;
} Forces llvm.loop.vectorize.enable = false. Use when the
auto-vectoriser produces strided scatter / gather code that
underperforms the scalar form.
@vectorize(width: N)
fn mulInto(out : i32[], a : i32[], b : i32[], n : i32) {
@vectorize(width: 8)
for i of 0..n { out[i] = a[i] * b[i]; }
} Pins the vector width. N must be a positive integer literal no
larger than the widest lane count the machine running the compiler
executes; an oversized request is rejected with E0292, which names
that maximum. See SIMD.
@unroll(N)
fn total(values : i32[], count : i32) : i32 {
let accum : i32 = 0;
@unroll(4)
for i of 0..count { accum = accum + values[i]; }
return accum;
} Sets llvm.loop.unroll.count = N. N must be a positive integer
literal.
Declaration annotations
Where each one applies — anywhere else it is refused (E0012), since
nothing would read it there:
| annotation | applies to |
|---|---|
@target(…) | any declaration, any method |
@link(…) | a function, a foreign (extern "C", bodiless) function |
@float(…) | a function, a method |
@heat(…) | a function, a method |
@layout(…) | a class, a struct |
@upsert(…) | a method |
@derive(…) | a class |
Each states its value once per declaration. A second @float or @heat naming a different word is refused (E0001); a field of @link or @layout may sit in a second annotation of the same name, but written twice it is
refused. @derive is the exception — a second one lists more traits — and a
second @target or @upsert is refused (E0001) rather than read past: write
one @target(os = …, arch = …) stating the whole predicate.
@link(field = …)
@link(symbol = "write")
extern "C" fn libc_write(fd : i32, buf : ptr<i8>, n : i64) : i64; How a declaration binds to the world outside the program, as named fields, each optional:
| field | value | effect |
|---|---|---|
symbol | a non-empty string literal | the symbol the linker resolves this declaration by, in place of the Axle identifier |
A field @link does not have is refused (E0011, the closest one
suggested); a @link that names nothing, a positional value, a field
written twice or a symbol that is not a non-empty string is E0001. See FFI.
Axes: @float(…), @heat(…), @layout(…)
Some annotations answer one question about a declaration, and take the answer
as their value — one bare word from a closed set. Two different values on the
same axis are a conflict (E0001); a word the axis does not have is refused
(E0011), with the closest one suggested. Writing a value on its own — @fast, @cold, @packed — is refused the same way, and the suggestion is
the axis form.
@float(fast | strict | contract)
@float(fast)
fn sum(values : f64[]) : f64 {
let total : f64 = 0.0;
for (i of 0..values.length) {
total = total + values[i];
}
return total;
} The function’s floating-point mode. fast enables every IEEE-754 relaxation
LLVM knows (reassociation, no NaN, no infinity, …), which is what lets a float
sum vectorise; strict refuses them all, even fusing a multiply-add; contract,
the default, allows the fused multiply-add and nothing else. See LLVM attributes → floating-point fast-math.
@heat(cold)
@heat(cold)
fn reportCorruptHeader(offset : i32) : i32 {
println("corrupt header at " + offset);
return -1;
} Declares the function rarely run. It is never inlined — however small its
body — and reaches LLVM marked cold, so a branch that calls it is laid out as
the unlikely one and the hot loop around it keeps its code tight. A function
whose every path throws is treated as cold already; @heat(cold) says so for
one that returns. On an inline fn it is a contradiction, refused (E0001).
The call it keeps is a real call, with its calling convention: in a loop, the values live across it must survive in callee-saved registers or on the stack. Mark the function you expect to run once in a long while, not one on the loop’s common path.
@layout(packed)
@layout(packed)
extern "C" struct WireHeader {
tag : u8;
length : u32;
} On an extern "C" record, refuses padding: every field sits right after the
previous one and the record aligns to 1 — for a layout whose bytes are the
contract, a wire format or an on-disk header. Without extern there is nothing
outside the program to read the bytes, and it is refused (E0751). See FFI → extern "C" struct.
@derive(Trait, …)
@derive(Eq, Ord, Hashable, ToString, Clone)
class Point {
x : i32;
y : i32;
constructor(x : i32, y : i32) {
self.x = x;
self.y = y;
}
} On a class, asks the compiler to synthesise a standard, field-by-field
implementation of each listed trait — Eq → equals, Ord → compareTo, Hashable → hashCode, ToString → toString, Clone → clone. The expansion happens at parse time, so the generated methods
flow through normal lowering and stay correct as the class gains fields.
The arguments are bare trait identifiers ; an unknown name, a quoted
argument (@derive("Eq")), or a collision with a hand-written method of
the same name is rejected at parse time. See Generics and traits → @derive for the full table and behaviour.
Unknown and misplaced annotations
Both annotation slots reject a name they don’t recognise, just through different mechanisms :
- Declaration slot (above a declaration, a method or a field) parses a
free
@Nameor@Name(args), then checks the name against the closed vocabulary and refuses anything outside it withE0011, naming the closest recognised annotation when one is a plausible typo away. A recognised one is also checked against where it applies (E0012) and against the value it takes. An annotation nothing reads would compile away with its intent unapplied, so it is refused instead. - Loop slot (above a
for/while/do-while) accepts only@vectorize,@vectorize(disable),@vectorize(width: N), and@unroll(N). Any other name in the loop slot is a parse error.
Limitations
| Limit | Why it holds |
|---|---|
| Every annotation value is a literal | @unroll(N) and @vectorize(width: N) take a positive integer literal and @link(symbol = "…") a non-empty string literal — a named constant is not read |
| The loop slot accepts four spellings and nothing else | @vectorize, @vectorize(disable), @vectorize(width: N), @unroll(N) — any other name above a loop is a parse error |
| An annotation applies to the declaration it sits above | it is read where it is written; nothing propagates it to callers or to the code a declaration is inlined into |
@vectorize(width: N)’s ceiling is the build machine’s | the check reads the feature set of the CPU the compiler runs on, not the triple a --target names |
| The loop annotations steer LLVM only | they set a loop metadata flag; a loop with a loop-carried dependence stays scalar whatever it is annotated with |
| An unknown name is refused, not ignored | an annotation nothing reads would compile away with its intent unapplied (E0011) |
The refused-placement rules and the value each word may take are under Unknown and misplaced annotations and Declaration annotations above.
See also
- Language tour — surface syntax overview.
- SIMD and auto-vectorisation — what
@vectorizeand@unrollactually do. - FFI — calling C from Axle —
@link(symbol = …)in context. - Concept index — every annotation on one page.