machdocs
Home GitHub

Decorators

A decorator is a codegen directive attached to a declaration. It controls how the compiler emits a symbol - its linker name, alignment, section placement, inlining, or PE import routing - and nothing else.

Note

Decorators are codegen-only. Visibility (pub / ext) is a separate concern and is never controlled by a decorator.

Surface

A decorator is written as an attribute clause. A bare flag takes no arguments; a directive takes comptime-expression arguments.

#[name]            # bare flag (e.g. inline)
#[name(args)]      # directive with comptime-expr arguments
Restriction

A line comment that begins #[ with no space opens an attribute. Write such a comment with a separating space: # [...].

Placement

Decorators appear before the declaration they target, one per line or space-separated on the same line. They attach to the immediately following declaration only and do not bleed across declarations.

#[inline]
#[symbol("big")]
fun big(a: i64, b: i64) i64 { ... }

#[align(64)] #[symbol("g_lit64")]
pub var g_lit64: u8 = 7;

Directives

The directive set is closed; new directives require a compiler change.

symbol(str) - linker name

Overrides the emitted or imported symbol name. Applies to functions and globals. Without it the compiler mangles the mach name; symbol gives the linker the exact name.

The mangled name is the source FQN as the source spells it - std.types.string.str_len, with generic arguments after a $ - so a profile, a crash report, and a disassembly all read the name you wrote. There is no prefix: a mangled name always contains a . and a C identifier never can. ext fun foreign symbols and #[symbol("...")] names are literal and unaffected.

#[symbol("main")]
fun entry(argc: i64, argv: **u8) i64 { ... }

#[symbol("write")]
ext fun libc_write(fd: i64, buf: *u8, n: i64) i64;

library(str) - dynamic import attribution

Pins an ext import to a specific dependency in the link set. Applies to ext functions only, and composes with symbol - the import is emitted under the renamed symbol within the named dependency.

#[library("ws2_32.dll")] #[symbol("WSAStartup")]
ext fun wsa_startup(ver: u16, data: *u8) i32;

inline - force inlining

Marks a function for inlining at every call site, overriding the compiler's size- and use-count heuristics. Applies to functions only and takes no arguments.

#[inline]
fun fast_path(x: i64) i64 { ret x * 2; }

It also crosses a module boundary. A dependency's ordinary function body never reaches an importer, so a four-instruction fetch_add in another module cost a call as well as its instructions. #[inline] declares that the body is part of the function's public surface, so an importer may materialize it - the same axis a generic, a comptime-param, and a pack-tailed function are already on, and the first member an author asks for rather than the type system forcing. The copy is emitted weak under the origin's own mangled name, so the defining module's definition still wins at link and the function stays one function with one address.

noinline - forbid inlining

The inverse of inline: forbids inlining a function into any caller, overriding the heuristics that would otherwise fold it in. Applies to functions only and takes no arguments.

#[noinline]
fun cold_path(code: i64) i64 { panic("unreachable state"); }

Use it to keep a function's frame and symbol real - for a profiler or stack sampler to attribute its cost correctly, to keep a cold path from bloating a hot caller's instruction cache, or to hold code size down on a constrained target.

align(expr) - alignment override

Sets the alignment of a global variable or a record/union type. expr must be a comptime integer - a literal, or a comptime expression such as $size_of(T) or $align_of(T).

#[align(64)]
pub var cache_line: u8 = 0;

#[align($size_of(Pair))]
pub var g_cmp: u8 = 0;

#[align(32)]
rec Over { a: u8; }

packed - no padding

Lays a rec or uni out with no padding: every field sits immediately after the previous one, there is no padding at the tail, and the type takes no alignment from its fields. Takes no arguments.

align only ever raises alignment. packed is the inverse, and it exists for the case where the layout is not mach's to choose - a C struct, a file header, a wire frame, a vertex whose stride a buffer fixes. Without it such a shape cannot be described as a record at all.

#[packed]
rec Header {
    magic:    u8;    # offset 0
    version:  u16;   # offset 1
    length:   u32;   # offset 3
    checksum: u64;   # offset 7
}                    # $size_of == 15, $align_of == 1

Naturally the same shape is 24 bytes. $size_of, $align_of, and $offset_of all report the packed layout, and so does the code that reads and writes the fields - there is one layout, not a declared one and an emitted one.

It composes with align rather than conflicting, and each owns one question: packed decides padding (none between fields, none at the tail) and align(N) decides the record's own alignment, rounding its size up to a multiple of N.

#[packed] #[align(8)]
rec Frame { a: u8; b: u32; }   # fields at 0 and 1; $align_of == 8, $size_of == 8

Packing is not transitive. A packed record packs its own fields; a record it contains keeps its own internal padding and is merely placed without padding. This matches C, and it is the rule that composes - an inner type's layout does not change depending on who holds it. A transitive rule would silently change the inner record's meaning inside its holder. If the inner record must be packed too, write #[packed] on it as well.

rec Point { x: u8; y: u32; }   # natural: y at 4, size 8

#[packed]
rec Msg { tag: u8; p: Point; } # p at offset 1, still 8 bytes; $size_of(Msg) == 9

What is refused

Target note: riscv64

Unaligned access is permitted-but-may-trap on RV64, and where the hardware does not do it Linux emulates the access in the kernel. A packed field access there is expected to be correct and pathologically slow - a trap-and-emulate round trip per access rather than a load. x86-64 and aarch64 do unaligned scalar access in hardware.

section(str) - object section placement

Places a function or global variable in a named section instead of the default .text / .data. The section is created if absent; cross-section calls and accesses use ordinary relocations.

#[section(".hottext")] #[symbol("f_hot")]
fun f_hot(x: i64) i64 { ret x + 1; }

#[section(".machsec")] #[symbol("g_sec")]
pub var g_sec: u64 = 100;

oblivious - constant-time boundary

Marks a function as a constant-time boundary. Applies to functions only and takes no arguments. Inside it the backend must not introduce a secret-dependent branch or select a variable-latency instruction on a secret operand; a translation validator re-derives the secret taint over the lowered MIR and rejects any such leak.

#[oblivious]
fun ct_eq(a: ^[8]u8, b: ^[8]u8) u8 { ... }
Experimental preview

The constant-time guarantee is not complete and has not been audited end to end. Do not build production cryptography on it at this version. See Secrecy for the known open holes.

scalar - opt out of auto-vectorization

Excludes a function from loop auto-vectorization, so its loops compile to scalar code even in the release pipeline on a vector-capable target. Applies to functions only and takes no arguments.

#[scalar]
fun reference_sum(a: *i64, n: usize) i64 { ... }

A #[scalar] function is also declined by the inliner, so the opt-out survives inlining - it cannot be lost by the body moving into an unflagged caller. Use it for a scalar reference twin in a differential test, or where vectorized codegen is undesirable for a specific function. The project-wide equivalent is the vectorize profile key.

naked - no prologue, no epilogue, body as written

Emits the function's body exactly as written and nothing else: no frame-pointer record, no stack allocation, no callee-save stores, no argument moves, and no return. Applies to functions only and takes no arguments.

#[naked] #[symbol("_start")]
fun start() {
    $if ($mach.build.arch == $mach.arch.x86_64) {
        asm x86_64 {
            mov rdi, [rsp]        # argc, straight off the kernel-supplied stack
            lea rsi, [rsp+8]      # argv
            call main
        }
    }
    $or { asm aarch64 { ... } }
}

The programmer owns the frame, the stack alignment, the link register, and the return. That is the whole point: a reset vector, an interrupt handler that must return with iret / rti rather than ret, a syscall or context-switch stub, or a thread entry point whose register state at entry is the interface.

Frame elision is a separate, automatic thing: the compiler already omits the prologue for a leaf that provably never touches its frame. naked is the declared form, and it is unconditional. Merely containing an asm block does not suppress a frame: a function that also makes a call gets one, since an unaligned call boundary or a clobbered link register is not something the author asked for by writing assembly.

embed(str) - compile-time file embedding

Sources a val's bytes from a file at compile time: the file's content is the initializer. Applies to val only - not var (the storage is read-only data) and not an ext data import (which has no storage here). Takes one string-literal argument.

#[embed("assets/logo.qoi")]
val LOGO: [_]u8;          # length taken from the file's byte count

#[embed("boot/sector.bin")]
val SECTOR: [512]u8;      # length pinned; a size change fails the build

Shader directives

Nine directives belong to the GPU pipeline surface: stage, workgroup, input, output, builtin, uniform, storage, sampler, and the two target-owned declarations handle and op. They are accepted on every target, because which target a module is built for is not a property of its source; only a target that forms pipeline stages acts on them. Each is documented with the surface it belongs to, on GPU shaders.

Applicability

Each directive targets a fixed set of declaration kinds. The full matrix:

Directivefunext funval / varrec / uni
symbolyesyesyesno
librarynoyesnono
inlineyesnonono
noinlineyesnonono
alignnonoyesyes
packednononoyes
sectionyesyesyesno
obliviousyesnonono
scalaryesnonono
nakedyesnonono
embednonoyesno
stageyesnonono
workgroupyesnonono
input / outputnonoyesno
builtinnonoyesno
uniform / storagenonoyesno
samplernonoyesno
opyesnonono
handlea bodyless def only

The val / var column is shared, but embed accepts only val - a var is refused.

See also

  • Visibility - pub / ext, the separate concern decorators never touch
  • Intrinsics - $size_of and $align_of as align arguments
  • Secrecy - the ^ qualifier oblivious is the codegen contract for
  • Inline assembly - what a naked function's body may contain
  • Manifest - the [link.X] requirements a library pin resolves against