Attributes

Built-in attributes

AttributeTargetEffect
@profile(t)fileSet strictness profile (t0, t1, t2)
@link("lib")moduleDeclare native library dependency
@export / @export("sym")fnExport with C calling convention
@inlinefnInline function at call sites (VM and native; native builds also inline small functions on their own). It changes no meaning: arguments convert as for a call
@constant_timefnChecked to have no secret-dependent branch, loop count or address (below)
@any_threadfnC may call it on a thread of its own; checked like a thread’s function (Linking & FFI)
@publicparameter of a @constant_time fn, struct fieldThe value is not secret
@keeps / @keeps(c)fn parameterThe function keeps a view of the argument past the call: a temporary there is an error (below)
@centroidfield of a #shader stage’s vertex outputInterpolated at a point the triangle covers, so multisampling never extrapolates it; its derivatives become less accurate (#shader)
@packedstructNo padding between fields, alignment 1
@align(N)structAlignment at least N (a power of two, at most 16)
@uninitializedlocal varIts storage is not zeroed: write before you read (Variables)
@opaquestructFields inaccessible outside the defining module
@swizzle pattern: TypestructEnable vector swizzle access

Built-in comptime intrinsics

These @name(...) forms run in @tape code; @sizeof and @alignof also work in runtime code:

IntrinsicReturnsDescription
@sizeof(T)i64Size of type in bytes
@alignof(T)i64Alignment of type
@typeof(T)TypeInfoType metadata (Reflection)
@has_field(T, "name")boolWhether struct has a field
@has_method(T, "name")boolWhether type has a method
@inner_type(T)typeInner type of pointer/slice/optional/array
@field_type(T, "name")typeType of a struct field
@assert(cond)voidCompile-time assertion (in a #test region, a test assertion; elsewhere in code that runs, use @trap or an if)
@embed("path")[]const u8Embed file at compile time
@print(...)voidPrint at compile time
@println(...)voidPrint with newline at compile time
@compile_error(msg)neverStop compilation with msg (diverges, like @trap)

Runtime builtins of note: @select(c, a, b) and @declassify(e) (constant-time functions), @cpu_has(f) and the AES and carry-less multiply instructions (Vectors), @trap("msg"), @move_bytes(dst, src, n) / @fill_bytes(dst, byte, n), memory’s block move and fill (Pointers), @offset(p, n), the address n elements past a pointer (Pointers), the codec vector operations @avg, @add_sat, @sub_sat, @madd, the interleaves and the even and odd lanes (Vectors), @sqrt(x), the machine’s square root, @mul_hi(a, b), the high 64 bits of the 128-bit product of two u64s or two i64s (a * b is the low half), @add_carry(a, b, cin, &cout) and @sub_borrow(a, b, bin, &bout), the low 64 bits of a + b + cin (a - b - bin) with the carry (borrow) out written to a u64 as 0 or 1, for multi-limb arithmetic (u64 operands; any non-zero carry in counts as 1; no branch, so they are allowed in @constant_time code; a chain of them through one carry variable, s1 = @add_carry(x1, y1, c, &c), compiles to a chain of add-with-carry instructions with the carry kept in the processor’s flags, as fast as hand-written carries or faster), and @atomic_load(p), @atomic_store(p, v), @atomic_add(p, v) and @atomic_cas(p, old, new), one indivisible, sequentially consistent operation on an i32, u32, i64 or u64 word that threads share (Pointers).

User-defined attributes

Declare custom attributes with the attr keyword:

tape
attr serializable;
attr deprecated(message: []const u8);

@serializable
struct Config {
    name: []const u8;
    value: i64;
}

@deprecated("use new_api() instead")
pub fn old_api() -> i64 {
    return new_api();
}

User attributes can be attached to functions, structs, struct fields, enum variants and tagged union variants. They accept up to 16 arguments. Attribute names cannot shadow built-in names (profile, runtime, link, export, inline, constant_time, public, keeps, centroid, asm, tape, emit, packed, opaque, align, sizeof, alignof, assert, typeof, print, println, compile_error).

Constant-time functions

Code that handles keys and other secrets must not let a secret decide when something happens: a branch, a loop count or a memory address that depends on a secret shows in the running time and in the cache. @constant_time makes the compiler check a function for that, and keep it true in the machine code it emits.

tape
@constant_time
pub fn ct_eq(a: []const u8, b: []const u8) -> bool {
    if (a.len != b.len) { return false; }   // lengths are public
    var diff: u8 = 0;
    for (i in 0..a.len) {                   // the loop count: public
        diff = diff | (a[i] ^ b[i]);        // secret bytes, public index
    }
    return diff == 0;                       // a comparison as a value
}

Parameters are secret unless marked @public, and so is everything computed from a secret. A struct field marked @public holds no secret. In a @constant_time function these are errors when they involve a secret:

  • if, while, a for range, match, is, or return, or { }, x!, == null (a branch or a loop count);
  • && and || (they branch: combine secret bools with &, |, ^ or @select);
  • an index, a slice bound or an integer cast to a pointer (an address);
  • / and %, unless dividing by a power-of-two literal;
  • float operations;
  • passing a secret to a function that is not @constant_time (an extern fn, a function pointer, string interpolation included) or to a @public parameter, storing one in a @public field;
  • using a module-level var at all.

The error names the rule and how the value became secret:

plaintext
pad.tape:4:9: error: branch on a secret in @constant_time fn 'pad': the condition reads 'n' (...)
  'n' is secret: line 3 assigns it '(block[63] & 63) as i64'
  'block' is secret: parameter 'block' (line 2) is not @public

Two builtins go with it:

BuiltinResult
@select(c, a, b)a if c else b; both evaluated, never a branch (integers, bools, enums, vectors; any function)
@declassify(e)e, now public: what may be revealed, such as whether a MAC verified (only in a @constant_time function)
tape
@constant_time
fn cswap(swap: bool, a: *u64, b: *u64) {
    let x: u64 = *a;
    let y: u64 = *b;
    *a = @select(swap, y, x);
    *b = @select(swap, x, y);
}

The machine code of a @constant_time function has no conditional branch its source does not have, on x86-64 and arm64, optimized or not; zeroing key material before returning is never optimized away. The timing promise is native code’s: tape run gives the same results, not the same timing. The bits rotations, byte swaps, loads and stores and bytes.copy, move, fill and zero are @constant_time, so constant-time code can call them.

Parameters that keep a view

A function that keeps a view of its argument past the call (a container storing the element, a setter storing a []const u8) marks the parameter @keeps:

tape
import str from "str";

var label: []const u8 = "";

fn set_label(@keeps s: []const u8) {
    label = s;                    // kept after set_label returns
}

fn main() -> i32 {
    set_label(str.from_view("hi"));   // error: the string is freed after this statement
    let name = str.from_view("hi");
    set_label(name);                  // fine: name lives on
    set_label("literal");             // fine: a literal is static
    return 0;
}

The call with a temporary is refused: “a temporary string would be freed while ‘set_label’ keeps a view of it (parameter ‘s’ is @keeps); bind it to a name first”. The temporaries are the ones a view may not be stored from (Ownership): an owned string a call returns, a concatenation, an interpolation, and a struct or array literal holding one. A named string, a literal and a value the callee takes by value (Vec_string.push(make()) copies the string) are fine.

  • @keeps goes only on a parameter whose type holds a view or a pointer (a slice, a pointer, or a struct, array or ?T holding one); on an i64 or a string it is an error.
  • @keeps(c) takes a compile-time bool, for generators: @emit pub fn Vec_$T.push(*self, @keeps(${fixed.holds_view(T)}) item: $T) keeps for Vec_cslice_u8, not for Vec_i64.
  • It is part of the signature: other modules’ calls are checked too, and @moduleinfo gives each parameter’s keeps. A call through a function pointer is not checked.
  • The stdlib’s containers mark their element-taking methods (push, set, insert, a map’s set key and value, …) when the element holds a view.

Qualified attributes

An attribute is private to its module unless declared pub attr. Attributes from other modules use dot syntax:

tape
import mylib from "mylib";   // mylib declares: pub attr serializable;

@mylib.serializable
struct Data { x: i64; }

Compile-time attribute access

In @tape code, a field descriptor from @typeof(T).fields answers .has_attr(a) and gives an attribute’s arguments with .attr(a), read by the parameter names of its attr declaration:

tape
attr serializable;
attr max_length(n: i64);

@tape fn count_serialized(T: type) -> i64 {
    var c: i64 = 0;
    for (field in @typeof(T).fields) {
        if (field.has_attr(serializable)) { c = c + 1; }
    }
    return c;
}

field.attr(max_length).n reads an argument; .attr() of a marker attribute (no parameters) is a compile error. See Reflection.

Last modified: