Axle v0.14.1

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-while loop. 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:

annotationapplies 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:

fieldvalueeffect
symbola non-empty string literalthe 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 @Name or @Name(args), then checks the name against the closed vocabulary and refuses anything outside it with E0011, 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

LimitWhy 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 aboveit 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’sthe check reads the feature set of the CPU the compiler runs on, not the triple a --target names
The loop annotations steer LLVM onlythey 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 ignoredan 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

annotationsattributesvectorizereference