16Directives and Attributes
Directives are compile-time annotations attached to declarations using ![...] syntax. They modify how the compiler treats the declaration without changing its semantic meaning at the call site.
![inline]
function hot_path(x: int) -> int {
return x * 2;
}
![repr(packed)]
type struct PackedHeader {
magic: u32;
version: u8;
flags: u8;
}
![align(16)]
type struct AlignedData {
data: f64;
}
16.1 Recognised Attributes
| Directive | Target | Description |
|---|---|---|
![inline] | function, method | Every call is inlined, at every optimization level and across modules: a module calling the function compiles its own copy of the body, which is discarded once inlined, and the defining module keeps the one definition. A generic function's instances are inlined the same way. |
![noinline] | function, method | The function is never inlined. |
![caller_location] | function, method | location() in the body is the place of the call (section 4.5). The function cannot be taken as a value, and cannot be variadic. |
![deprecated] / ![deprecated("msg")] | any decl | Marks a declaration as deprecated. Parsed and validated; use-site warnings are not yet emitted. |
![symbol("name")] | extern fn / method / fn definition | Override the linker symbol: Cryo callers use the declared name while the symbol emitted / resolved at link time is name. On an extern decl or body-less method it renames the imported symbol; on a function definition it renames the emitted symbol. Used by the C++ binding generator to pin a decl to its Itanium-mangled symbol. |
![no_mangle] | function / method | Emit the definition under its bare declared identifier instead of the mangled C$… symbol, so it can be linked from C (or serve as a runtime lang-item) without a wrapper. The preferred unmangling directive; ![symbol("name")] is for the rename case. |
![weak] | function / method | Emit the definition with weak linkage (and an any-selection COMDAT), so a strong definition elsewhere overrides it. For user-overridable runtime symbols (allocator, panic hook). |
![section("name")] | function / method | Place the emitted symbol in object-file section name. |
![naked] | function | Emit no prologue/epilogue (sets LLVM's naked attribute); the body must be a single asm { … } block that provides its own ret/jmp/exit. For a freestanding _start and other raw entry points. A non-asm body is an error (E0151). |
![allow(name)] / ![warn(name)] / ![deny(name)] | any decl | Intended to adjust a named lint's level. Parsed and validated; lint-level adjustment is not yet implemented. Each takes at most one reason = "...", a non-empty string; a lint name is not checked, so an unknown one is accepted. |
![derive(Trait, ...)] | struct / enum | Intended to auto-derive one or more traits. Parsed and validated; no trait is actually synthesized. See section 21. |
![sink] | method | Marks a method as consuming its receiver, even when the receiver is syntactically &this or mut &this. Useful for methods that semantically take ownership but want the borrow-style call ergonomics. |
![implicit] | generic function / method | Lets a call omit its generic type arguments: the compiler recovers them by unifying the declared return type against the call's expected type (the type of the const x: T = ... it initialises). Every type parameter must appear in the return type. A call with no expected type reports E0307; write the arguments explicitly (f::<T>(...)) there. Used by VaArgs::next so const n: i64 = va.next() reads cleanly. |
![config(<atom>)] / ![target(<atom>)] / ![<atom>] | any decl | Platform / build-flavor gate. <atom> is windows, linux, macos, unix, or not(<atom>). The bare-atom form (![windows]) is sugar for ![config(windows)]. The decl is stripped from the AST when the gate doesn't match. |
![repr(C)] / ![repr(packed)] / ![repr(transparent)] | struct / union / enum | Memory layout control. See section 16.3. |
![align(N)] | struct / union / enum / variable | Minimum alignment in bytes; N must be a power of two. See section 16.3. |
![arch(<arch>, <dialect>)] | asm block | Required directly above an asm { ... } block (see section 6.12). Names the target architecture - which gates the block, like ![config] - and the assembly dialect (intel or att). |
![clobber(...)] | asm block | Lists registers, flags, or memory that an asm block overwrites but does not bind as operands. |
The test-mode directives (![config(testing)], ![test], ![ignore], ![should_panic]) are documented in the next subsection.
Other names that appear with ![...] syntax are accepted by the parser and recorded so user tooling can observe them, but the compiler emits a warning for any non-builtin name and applies no semantics. A list of reserved-but-unimplemented directive names is in section 21.
16.2 Test Directives
The test framework introduces a small set of directives recognised by the compiler (section 20 covers usage):
| Directive | Description |
|---|---|
![config(testing)] | Marks a file as a test source. May appear next to its namespace. |
![test] | Marks a function as a discoverable test. |
![ignore] | Skips the test unless cryo test --ignored is passed. |
![should_panic] | Asserts the test panics; passing the test means observing a panic. |
16.3 Memory Layout
Cryo is a systems language and gives programmers explicit control over the in-memory layout of user-defined types. Layout matters for FFI (matching a struct declared in a C header), for binary protocols (network packets, on-disk records, hardware register maps), and for memory-conscious data structures.
16.3.1 The Default Layout (repr(Cryo))
Without any explicit directive, a struct has the default Cryo layout:
- Fields are laid out in source order.
- Each field is placed at the lowest offset that satisfies its natural alignment.
- The size of the type is rounded up to the type's alignment (the maximum alignment among its fields, or 1 for empty types).
- Padding bytes inserted to satisfy alignment have undefined content.
The default layout currently matches the platform's C ABI for primitive-typed fields. The language reserves the right to optimise the default layout in future versions (e.g. reordering fields). Do not rely on the default layout for FFI or binary serialisation; use ![repr(C)] for those cases.
16.3.2 ![repr(C)]
![repr(C)]
type struct timespec {
tv_sec: i64;
tv_nsec: i64;
}
![repr(C)] guarantees that the type's layout matches the platform's C ABI:
- Fields are laid out in source order.
- Padding follows the C rules: each field starts at the lowest offset that is a multiple of its alignment.
- The size of the type is rounded up to the type's alignment.
- The compiler will not reorder fields under any circumstances.
![repr(C)] is the correct choice for any type that is exchanged with C code, mapped to a C header, or used as a binary record. Combine with ![align(N)] to over-align such a struct.
16.3.3 ![repr(packed)]
![repr(packed)]
type struct PacketHeader {
version: u8;
flags: u8;
length: u32; // no padding before this field
crc: u32;
}
![repr(packed)] removes all inter-field padding:
- Fields are laid out in source order.
- Each field is placed at the next byte offset, regardless of its natural alignment.
- The type's alignment is 1.
- The size of the type equals the sum of its fields' sizes.
A repr(packed) type may contain misaligned fields. Taking a Cryo reference (& or mut &) to a field of a repr(packed) type is a compile error: the reference would not be naturally aligned for its referent, which is undefined behaviour. Read or write the field by value instead; the compiler emits the unaligned load/store at the use site.
![repr(packed)] and ![align(N)] are mutually exclusive on the same type.
16.3.4 ![repr(transparent)]
![repr(transparent)]
type struct Pid {
inner: i32;
}
![repr(transparent)] declares a wrapper type whose layout is identical to a single contained field:
- The type must have exactly one field whose size is non-zero.
- The wrapper has the same size, alignment, and ABI as that inner field.
- A
Pidand ani32are interchangeable at the ABI level - passingPidto anextern "C"function is identical to passingi32.
This is the recommended idiom for type-safe wrappers around primitive FFI types (file descriptors, error codes, opaque handles) where the wrapper exists purely for type discipline at the source level.
16.3.5 ![align(N)]
![align(64)]
type struct CacheLine {
payload: [u8; 64];
}
![align(N)] sets the minimum alignment of the type to N bytes:
Nmust be a power of two between 1 and 65536.- The actual alignment of the type is
max(natural_alignment, N). - The size of the type is rounded up to its alignment.
![align(N)] is compatible with ![repr(C)] and ![repr(transparent)]; it is mutually exclusive with ![repr(packed)].
![align(N)] on a variable raises that variable's alignment for the lifetime of its storage; this is useful for stack buffers that must satisfy SIMD or hardware-register alignment requirements.
16.3.6 Enums
A unit enum (no variant payloads) has a discriminant whose type defaults to i32. The discriminant type can be set explicitly with the type-annotation syntax (not a directive):
type enum Color : u8 {
Red = 0;
Green = 1;
Blue = 2;
}
An ADT enum (variants with payloads) is laid out as a tag (i32) followed by a payload area sized to the largest variant, with padding so the payload area is naturally aligned. ![repr(C)] on an ADT enum is reserved syntax and currently produces the same layout as the default.
16.3.7 Inspecting Layout: sizeof(T) and alignof(T)
sizeof(T) and alignof(T) return compile-time u64 constants reflecting the type's chosen layout - including any ![repr] or ![align] directives applied to it. They are the recommended way to verify FFI struct layout against a C header in a test:
import std::test::assert::{ expect_eq };
import std::test::error::{ TestError };
![test]
function timespec_matches_c() -> Result<(), TestError> {
expect_eq(sizeof(timespec), 16 as u64)?;
return expect_eq(alignof(timespec), 8 as u64);
}
function name<T>(param: Type) -> Ret { ... }Declares a function. Parameters are written name: Type and are never inferred; the return type follows -> and defaults to void. Declaration order does not matter: every signature is collected before any body is checked, so functions may call each other freely and recurse.
function hot_path(x: int) -> int(parameter) x: intreturn value;Leaves the current function with the given value. A function whose return type is not void must return one; a bare return; is for void. In an async function it completes the future with the value.
(parameter) x: inttype struct Name { ... }
type enum Name { ... }
type trait Name { ... }
type union Name { ... }Begins a type declaration; the keyword that follows says which kind. On its own, type Name = Existing; declares an alias.
type struct Name {
field: Type;
method(&this) -> Ret { ... }
}A value type with named fields and optional methods. Structs live on the stack and are passed by value. Fields are public by default and may carry = default values; methods take their receiver as &this, mut &this, or this.
type struct PackedHeaderPackedHeader.magic: u32PackedHeader.version: u8PackedHeader.flags: u8type struct AlignedDataAlignedData.data: f64type struct timespectimespec.tv_sec: i64timespec.tv_nsec: i64type struct PacketHeaderPacketHeader.version: u8PacketHeader.flags: u8PacketHeader.length: u32PacketHeader.crc: u32type struct PidPid.inner: i32type struct CacheLineCacheLine.payload: [u8; 64]type enum Name {
Unit,
Payload(T),
Named { x: i32 },
}An algebraic data type. Variants may be bare or carry a tuple or struct payload, and a match on an enum must cover every variant. Name::Variant names a variant; methods are added with implement enum Name { ... }.
type enum Color : u8Color::RedColor::GreenColor::Blueimport a::b;
import a::b::{ X, Y };
import a::b as c;
import a::b::*;Brings a module, or selected items from it, into scope. One path per declaration: use the brace form for several items, as to alias, and * for everything public (prefer the brace form; wildcards invite name collisions).
function expect_eq<T>(a: T, b: T) -> Result<(), TestError>
where T: Eq + DisplayOk(()) if a.equals(&b), otherwise Err carrying a multi-line "assertion left == right failed" message that Display-formats both values. Allocates the message on the heap; on allocation failure during message building, returns a generic fallback message.
type enum TestError {
Failed(String);
Other(String);
}Why a test failed. The runner prints the variant's message verbatim to stderr from the child process before exiting non-zero, so the parent can show the failing message in the test summary.
std::test::errorfunction timespec_matches_c() -> Result<(), TestError>primitive ()The unit type: a value that carries no information, and the empty tuple. Distinct from void, which is the absence of a value - Result<(), E> succeeds with nothing to return. 0 bytes.
sizeof(T)The size of a type in bytes, as a compile-time constant - usable in static_assert. alignof(T) gives the alignment.
type struct timespecvalue as TypeExplicit conversion: between numeric types, or reinterpreting one pointer type as another. Cryo never converts implicitly, and as inserts no range checks - a narrowing cast is the programmer's responsibility.
value?Error propagation. On a Result<T, E> it yields the T of an Ok and otherwise returns the Err(e) from the enclosing function unchanged; on an Option<T> it yields the T of a Some or returns None. The enclosing function must return a matching Result / Option.
alignof(T)The alignment of a type in bytes, as a compile-time constant - usable in static_assert. sizeof(T) gives the size.
intrinsic function location() -> Location![arch(x86_64, intel)]
asm {
mov ${=out}, ${in}
}Embeds raw target assembly as an LLVM inline-assembly call. A preceding ![arch(<arch>, <dialect>)] directive is mandatory and gates the block by target; Cryo values are bound into the text through ${...} operand holes. At module scope, with no operands, it emits module-level assembly.
&this
mut &this
thisThe receiver of a method. Its first parameter declares how the method takes it: &this borrows read-only, mut &this borrows exclusively, and this / mut this move the value in and consume it. In the body, this.field reaches the members.
mut &this
mut &TypeAn exclusive, mutating borrow. As a receiver, mut &this lets the method modify the fields - and callers can see that from the signature alone. As a parameter type, mut &T lets the callee mutate through the reference while the caller keeps ownership.
const NAME: Type = value;At module level, a true compile-time constant, conventionally SCREAMING_SNAKE_CASE. (mut at module level declares mutable global state instead.)
const x: Tfunction name(count: i32, args...)A variadic parameter: the trailing bucket receives any number of further arguments with C's calling convention, so the function stays ABI-compatible with the printf family. Wrap the bucket in VaArgs to read typed values from it.
type struct VaArgs {
ptr: void*;
}Single-pointer wrapper over the active va_list. ptr is the raw va_list pointer - the args identifier the compiler binds in a variadic body.
VaArgs::next<T>(&this) -> T
where T: VaArgRead the next variadic argument as T (one of the promoted scalar types), advancing the list. ![implicit] lets the type be recovered from the call's expected type, so const n: i64 = va.next(); works; va.next<i64>() is the explicit form.
const n: i64namespace app::module;Every source file opens with one: its position in the module hierarchy. The compiler uses it to resolve imports and to mangle symbol names, so two parse functions in different modules never collide at link time.
type struct Pidextern function exit(code: i32) -> void;
extern "C" {
function puts(s: string) -> i32;
}Declares a function whose body the linker supplies - typically a C library symbol. The compiler trusts the Cryo signature; keeping it in step with the real C signature is on you.
language referencefunction max<T>(a: T, b: T) -> T
where T: Ord