Attributes and Builtins
Attributes
Section titled “Attributes”Attributes change the behavior or the layout of a declaration, and each one starts with #.
Layout Attributes
Section titled “Layout Attributes”#packed: for astruct. It packs the fields tightly with no padding, so the alignment is 1. Sub-byte fields (u1–u7,i1–i7) take only their bits, and so does every integer field after them. Any other field starts at a whole byte (see Packed structs).#align(N): C’salignas(N)for astruct.Nis an integer literal and a power of two, from 1 to 4096. The struct is aligned to at leastN, and its size is rounded up to a multiple ofN. That holds wherever the struct lives: as a local or a global, as a field of another struct (placed at anN-aligned offset), or as an array element. Example:#align(64). On a#packedstruct only the size is rounded up, and the fields stay back to back.core::sizeofagrees with this at compile time and at run time.
Function Attributes
Section titled “Function Attributes”#inline: hints to the compiler that it should inline the function.#noinline: keeps the compiler from inlining the function.#noreturn: says that the function never returns, as with exit or panic.#naked: produces a function without the standard prologue or epilogue. You will mostly use it for raw inline assembly entry points.#entry: makes the function the program’s entry point, as an alternative to naming itmain.#interrupt: an x86-64 interrupt or exception handler,fn(frame: *const x86::InterruptFrame)or, for an exception with an error code,fn(frame: *const x86::InterruptFrame, code: u64). The CPU calls it: it saves every register it uses and returns withiretq. Code can’t call it;core::fn_ptr(f)is the address for the interrupt table (std.x86.Idt). On aarch64,std.aarch64.set_exception_handlertakes an ordinary function instead.#cold: marks a function that is rarely called, so the optimizer moves it off the hot path. It becomes a real LLVMcoldattribute.#section("name"): places the function in a specific section of the object file.#keep: never strips the function, even when nothing uses it. It forces non-internal linkage.
FFI & Linkage
Section titled “FFI & Linkage”#export("name"): exports the symbol with external linkage. Without a string, it exports the symbol under its Fels name. It works on a function and on a top-level global (#export("plugin_entry") ENTRY: PluginEntry = .{ ... };), see 11_c_interop.#extern("library_name", "symbol_name"): declares a binding to an external function (FFI). The second argument is the real link symbol. The Fels declaration name does not have to match the C symbol, so a binding can take an idiomatic Fels name:Two externs may bind the same C symbol under different Fels names, and the one external declaration is deduplicated at link time. A plain Fels function may even share its name with a renamed extern’s C symbol, for example a#extern("kernel32", "WriteFile")WriteFile :: fn(...) -> bool; // Fels name == C symbol#extern("ws2_32", "connect")sys_connect :: fn(...) -> i32; // Fels name `sys_connect`, links `connect`connectwrapper oversys_connect. The wrapper keeps internal linkage under a module-private symbol, so the two don’t clash.#foreign: an alias for#extern.#system_library("libname"): a top-level directive that tells the linker to link against the named system library.#link_name("name"): sets the function’s external symbol name without exporting it (#exportdoes both).#weak: emits a weak symbol, which another definition can override at link time.
Diagnostics
Section titled “Diagnostics”#deprecated("message"): triggers a deprecation warning at the call site.#must_use: the caller may not discard the function’s return value.f();as a statement is an error, so writex := f()or_ := f().#maybe_unused: marks a declaration as possibly unused. The compiler accepts it, and it suppresses any future unused-declaration warning.
Threads
Section titled “Threads”#spawnson a function: the closures it takes run on other threads while the caller goes on (thread::Scope.spawn,thread::Pool.range). What such a closure captures is checked where the function is called: a value, a*const Tor[]const T, a pointer to a#synctype, or a struct of such. A*Tor[]Tis an error, since two threads could write through it at once, and so is a closure held in a variable, whose captures are not known there. Insideunsafe { }nothing is checked. See std.thread.#syncon a type: threads may use one at once through a pointer to it, as its methods synchronize (sync::Mutex,sync::Channel(T),atomics::Atomic(T)). A closure that runs on another thread may capture a*to it.
Builtins: the core:: namespace
Section titled “Builtins: the core:: namespace”Builtins live in the reserved core:: namespace. They are operations the compiler
provides, and they are always in scope, with no import. The core:: prefix makes a builtin
call look different from an ordinary function call:
n := core::sizeof(Point); // a builtin: you can see the compiler is involvedname := core::type_name(Point); // "Point"buf := unsafe core::slice_raw(u8, p, len);if bad { core::panic("invalid state"); } // no-returncore is reserved, so a module or import of your own may not be named core (that is a
compile error). Some builtins need an unsafe block, as the last column of the table shows.
Bare names are rejected. If you call a builtin by its bare name (
sizeof(T),ptr_from_int(...), …), you get a hard error that points you to thecore::form. A few names were tidied up in the move:typeid_of→core::type_id,truncate_to→core::narrow,slice_from_raw_parts→core::slice_raw. The old names still work behindcore::(core::typeid_of), but these docs use the new ones. (@panickeeps its sigil and also works ascore::panic.)
| Builtin | Signature | Unsafe? | Description |
|---|---|---|---|
core::sizeof |
core::sizeof($T: type) -> usize |
No | Size of T in bytes, padding included. |
core::align_of |
core::align_of($T: type) -> usize |
No | Alignment T requires, in bytes. Folds at compile time. |
core::offset_of |
core::offset_of($T: type, "field") -> usize |
No | Byte offset of field in struct T, with the field as a string literal. Folds at compile time. |
core::has_padding |
core::has_padding($T: type) -> bool |
No | Whether a T has bytes that no value sets: padding between or after struct fields (nested ones too), an optional’s flag byte, the bytes a smaller enum or error payload leaves. Folds at compile time. |
core::narrow |
core::narrow($T: type, val) -> T |
No | Truncate a wider integer into a narrower T. |
core::type_name |
core::type_name($T: type) -> []const u8 |
No | The type’s name as a string. |
core::type_info |
core::type_info($T: type) -> TypeInfo |
No | Compile-time reflection data (fields, variants, …). |
core::type_id |
core::type_id($T: type) -> usize |
No | Stable runtime type id. |
core::any |
core::any(x) -> Any |
No | Wrap a value into a type-erased Any. |
core::panic |
core::panic(msg: []const u8) -> never |
No | Abort with a message. Never returns. |
core::atomic_load |
core::atomic_load(ptr, ord) -> T |
No | Atomic load with ordering ord. |
core::atomic_store |
core::atomic_store(ptr, val, ord) |
No | Atomic store. |
core::atomic_add/_sub/_and/_or/_xor |
core::atomic_add(ptr, v, ord) -> T |
No | Read-modify-write that returns the previous value. |
core::atomic_max/_min |
core::atomic_max(ptr, v, ord) -> T |
No | Atomic max/min (signed or unsigned, chosen from T). |
core::atomic_exchange |
core::atomic_exchange(ptr, v, ord) -> T |
No | Swap that returns the previous value. |
core::atomic_cas |
core::atomic_cas(ptr, expected, desired, ord) -> T |
No | Compare-and-swap that returns the value it saw (== expected on success). |
core::atomic_fence |
core::atomic_fence(ord) |
No | Memory fence. |
core::ptr_from_int |
core::ptr_from_int($T: type, addr: usize) -> T |
Yes | Integer address → pointer T. |
core::fn_ptr |
core::fn_ptr(f) -> *void |
Yes | Raw thin pointer to a top-level function, for a C callback or a thread entry. An ordinary Fels fn value is a fat closure, which a C ABI can’t call. |
core::slice_raw |
core::slice_raw($T, ptr, len) -> []T |
Yes | Build a slice from a raw pointer and a length. |
core::volatile_store |
core::volatile_store(ptr: *T, val: T) |
Yes | Volatile memory write. |
core::unaligned_read |
core::unaligned_read($T: type, ptr) -> T |
Yes | Read T from a possibly-unaligned address. |
core::asm |
core::asm(...) |
Yes | Inline assembly: core::asm(volatile, "inst", inputs: {}, outputs: {}, clobbers: {}) |
The trailing ord is an integer ordering constant: 0 relaxed, 1 acquire, 2 release,
3 acq_rel and 4 seq_cst. It must be known at compile time. Prefer the
std.atomics wrappers, which default to seq_cst.
Source-location & target constants
Section titled “Source-location & target constants”These are compile-time constants, written without parentheses. Each one folds to a literal where you use it:
| Constant | Type | Value |
|---|---|---|
core::file |
[]const u8 |
the current source file path |
core::module |
[]const u8 |
the module name (file basename, no .fels) |
core::func |
[]const u8 |
the enclosing function’s name |
core::line |
i32 |
the line of the core::line reference |
core::column |
i32 |
the column |
core::os |
[]const u8 |
the OS the program is built for ("windows", "linux"), as TARGET.os |
core::arch |
[]const u8 |
the architecture the program is built for ("x86_64"), as TARGET.arch |
log :: fn(msg: []const u8) { println("{core::file}:{core::line}: {msg}"); }For ints and floats there are core::min(a,b), core::max(a,b), core::abs(x) and
core::clamp(x, lo, hi). The float functions are core::sqrt, core::floor, core::ceil,
core::round, core::trunc, core::sin, core::cos, core::pow(x, y) and
core::fma(a, b, c). Each one returns the type of its first argument and maps to a fast CPU
instruction. All of them except fma also fold at compile time, so you can use them in
#run.
Bit operations
Section titled “Bit operations”These work on any integer: core::count_ones(x), core::count_zeros(x),
core::leading_zeros(x), core::trailing_zeros(x), core::swap_bytes(x),
core::reverse_bits(x), core::rotate_left(x, n) and core::rotate_right(x, n). Each one
returns the type of x. Only core::count_ones folds at compile time. The others depend on the
width of x, and the comptime VM cannot run them yet. A #run expression inside a function
that uses one is compiled as ordinary code and computed at run time. A top-level #run
constant or an #assert that needs one is a compile error.
Memory & control
Section titled “Memory & control”core::memcpy(dst, src, n), core::memset(dst, byte, n), core::trap(),
core::unreachable(), core::prefetch(ptr) and core::cycle_count() -> u64 are the low-level
primitives that the standard library wraps.
Compile-Time Directives
Section titled “Compile-Time Directives”Compile-time If (#if)
Section titled “Compile-time If (#if)”#if evaluates a condition at compile time and keeps only the branch that is active. In a
function body it chooses between pieces of code, and the condition can be any constant
expression:
width :: fn() -> i32 { #if core::sizeof(usize) == 8 { return 64; } else #if core::sizeof(usize) == 4 { return 32; } else { return 16; }}At the top level it chooses between declarations: an #extern, an #import, a constant or a
struct layout that differs from one platform to the next.
#if TARGET.os == .windows { #extern("kernel32", "GetCurrentProcessId") pid :: fn() -> u32; PATH_SEP :: 92; // '\\'} else #if TARGET.os == .linux { #extern("c", "getpid") pid :: fn() -> i32; PATH_SEP :: 47; // '/'}A top-level #if is decided while the file is read, before any constant has a value. That is
why its condition can only ask about the build. It may use TARGET.os or TARGET.arch
compared with a variant (==, !=), TARGET.debug, true and false, joined with !, &&
and ||. Anything else in the condition is a compile error, and so is a platform that TARGET
does not know. The declarations in the branches not taken are parsed, but they are not part of
the program.
Embedding a file (#embed)
Section titled “Embedding a file (#embed)”#embed("path") is the bytes of a file, a []const u8 in the program’s static data. The path is
relative to the folder of the file that names it, and the file is read as the program is
compiled, so a change to it builds the program again.
GREETING :: #embed("data/greeting.txt");LINES :: #run count_lines(#embed("data/greeting.txt"));What compile-time code may read is a capability (09): the
project’s own code reads the files below the project’s folder, and a package’s code what its
#use line grants it (may read("data")). The bytes are text, never a format: printing them
shows their braces as they are.
Compile-time Run (#run)
Section titled “Compile-time Run (#run)”#run executes Fels code on the comptime VM while the program compiles, and folds the result
to a constant. #run expr is an expression, and it may initialise a top-level constant or a
local. As a statement (#run { … } or #run f();) it is an error: a statement run while
compiling would have nothing to give the function, and it used to run with the program
instead. To check something while compiling, use #assert.
compute_size :: fn() -> i32 { return 16 * 4; }
make_crc_table :: fn() -> [256]u32 { table: [256]u32 = .{}; for i in 0..256 { c := i as u32; for k in 0..8 { c = if c & 1 != 0 { 0xEDB88320 ^ (c >> 1) } else { c >> 1 }; } table[i] = c; } return table;}
// As an expression, including at the top levelSIZE :: #run compute_size();
// A whole table or struct: the result becomes the global's initial value, as data in the imageCRC: [256]u32 = #run make_crc_table();
// And in a function, for a valuemain :: fn() -> i32 { n := #run compute_size(); return SIZE + n + (CRC[1] & 0) as i32;}What a #run can return, what it can read (static data, read-only) and what gets refused is
explained in 09_comptime.
Note: the comptime VM supports many of the standard language features, but it may be limited
when it works with external or deeply complex types.
Compile-time assertions (#assert)
Section titled “Compile-time assertions (#assert)”#assert(condition);#assert(condition, "message");#assert is a standalone top-level directive, like #system_library, and it fails the build
when condition is false. The condition is evaluated on the comptime VM, so it can use
constants, core::sizeof, core::align_of, core::offset_of and calls the VM can run. This is
how you pin down a layout that something outside Fels depends on:
Header :: struct { kind: u8, len: u32, id: u64 }
#assert(core::sizeof(Header) == 16, "Header is sent as-is over the wire");#assert(core::offset_of(Header, "len") == 4);#assert(core::align_of(Header) == 8);When an assertion fails, it reports the message and the condition as you wrote it:
header.fels:3:9: error: static assertion failed: Header is sent as-is over the wire (`core::sizeof(Header) == 16`)- The condition must be a
bool, or you get a type error, and it must fold at compile time. A condition that needs the host, such as a call into a DLL, is an error, not a pass. #assertis evaluated when the program is lowered, that is byfels build,objectandir.fels checkonly type-checks, so it does not run assertions.- It works at the top level only. Inside a function, use
#ifwithcore::trap()or an ordinary check.
The TARGET constant
Section titled “The TARGET constant”TARGET describes the platform and the mode that the program is built for. It is a constant,
TARGET : TargetInfo. Its os and arch fields are enums, and debug is a bool:
TARGET.os: aTargetOs, which is.windows,.linux,.macosor.unknown.TARGET.arch: aTargetArch, which is.x86_64,.aarch64or.unknown.TARGET.debug:truein a debug build (-O0, thedebugmode), andfalsewhen the build is optimized.
describe :: fn() -> i32 { #if TARGET.os == .windows { return 1; } else { return 2; }}TARGET belongs to the build, not to the compiler, so a --target linux build made on Windows
sees .linux. A misspelt platform is a compile error (TARGET.os == .windos: “enum TargetOs
has no variant .windos”). In #if the comparison folds, and only the branch that holds is
compiled. In a plain if it is an ordinary comparison of constants. A build script and
fels check see the machine they run on.