Annotations and Compiler Builtins
@ marks something the compiler handles, not the runtime. It is the whole mechanism — there is no preprocessor, no #[cfg], no macro system. Everything spelled with an @ is one of the constructs below.
The attribute reference below is generated from the compiler's own vocabulary: it is the text milo lang --json publishes and an editor shows on hover, so it cannot drift from what the checker enforces.
Attributes
An attribute sits above a declaration.
@derive(…)
Goes on: struct.
Generate an implementation the compiler can write itself, e.g. @derive(Eq).
@noCopy
Goes on: struct.
This type is move-tracked however plain its fields are. A resource handle is often an integer, so the all-fields-Copy rule would make it Copy and move checking would never engage for exactly the type most likely to be used after release. Only needed when the release is NOT a Drop impl, since Drop already forces non-Copy.
@copy
Goes on: struct, enum.
This struct or enum is Copy although it holds a raw pointer: it does not own what the pointer points at. A pointer field or variant payload otherwise makes the type move-tracked, so an owning handle cannot be duplicated by accident; @copy is the explicit claim for a C-owned record or a view into a buffer some other value owns. Rejected on a type with no raw pointer in it.
@copyOnly(…)
Goes on: struct, fn.
A generic whose type parameters may only be instantiated with Copy types. The dual of @noCopy: a container that moves elements through a raw pointer (std/shard's Shard<T>.get returns self.base[i] bitwise) would hand out a second owner of a heap-owning T, so Shard<string> is rejected at the instantiation, not at runtime. Bare, it constrains every type parameter; @copyOnly(T) names the ones it applies to.
@copyOut
Goes on: fn, method.
This generic hands a T out of a container by copy (Some(self.data[i])). The copy is structural and never runs a Drop, so it exists only for a T that carries no Drop or @noCopy anywhere inside it: on a generic struct the method is absent from an instantiation with such a T (calling it names this reason), and a generic fn is rejected at the call. The rest of the type stays usable, which is what @copyOnly on the whole struct could not give a container whose other methods borrow.
@ordered(…)
Goes on: fn, method.
This generic compares a T with <, which exists only on integers, floats and strings. On a generic struct the method is absent from an instantiation with any other T (calling it names this reason), and a generic fn is rejected at the call, so BinaryHeap<Task> keeps its comparator constructor and loses only the natural-order one, instead of failing inside std on a < the caller never wrote. Bare, it constrains every type parameter; @ordered(K) names the ones it compares.
@sql(…)
Goes on: fn, method.
The named string parameter is SQL checked at compile time: at every call the argument must be a compile-time constant (or uncheckedSql(...), or the caller's own @sql parameter), and it is prepared against the schema the program's migrations build, so a syntax error, an unknown table or column, or a wrong count of bind arguments is a compile error at the literal. @sql(sql, args) also names the Vec<SqlArg> parameter whose array-literal length must match the query's ?s.
@table(…)
Goes on: struct.
The SQL table a @derive(Table) struct maps: @table("users"). A plain identifier; it is pasted into the generated SQL.
@sqlSchema(…)
Goes on: fn, method.
@sqlSchema(version, sql): each call is a migration whose constant SQL the compiler applies, in version order, to an empty database to learn the schema @sql queries are checked against. Carried by std/migrate's Migration.sql and Migration.baseline.
@cLayout(…)
Goes on: struct.
Check this struct's field layout against the real C header at build time.
@cSig(…)
Goes on: extern, fn.
Check an extern's signature against the real C header, e.g. @cSig("unistd.h", "long sysconf(int)"). Milo's types cannot express C type identity, so the header is the oracle.
@cValue(…)
Goes on: global.
Check a transcribed integer constant against the C macro or enumerator it mirrors, e.g. @cValue("SEEK_END", "stdio.h") on pub let SEEK_END: i64 = 2. Goes on an immutable global whose initializer is an integer literal: a wrong pixel format or scancode otherwise links fine and runs wrong.
@cOpaque
Goes on: field.
This extern struct field is filler with no C counterpart, so @cLayout skips it. For a struct padded out to the size C dictates (getrusage writes 144 bytes into a struct whose named fields cover 32); the field still counts toward Milo's own layout, so the size check stays meaningful.
@cName(…)
Goes on: field.
The C name of this extern struct field, for a C field whose name Milo cannot spell: @cName("type") kind: u32 is checked by @cLayout against the C field type, a Milo keyword. The Milo code reads and writes kind; the C header build --emit-header writes spells it type.
@iter
Goes on: field.
for x in wrapper walks this field exactly as it would walk the field itself: same bindings, same borrow, nothing allocated. Marks a Vec, HashMap, array or string field, at most one per struct.
@json(…)
Goes on: field.
Rename this field on the wire, e.g. @json("legacy_id"), in a struct that has @derive(Json). Rejected on a struct that does not derive Json, since nothing else reads it.
@wrapping
Goes on: fn, method.
Arithmetic in this routine wraps instead of trapping on overflow. For code that is inherently modular — an emulator's ALU, a hash mixer, a PRNG — so it need not spell wrappingAdd at every operation. @!wrapping applies it to a whole file.
@pure
Goes on: fn, method.
This function reads no global or module state; the checker enforces it.
@thread
Goes on: fn, method.
This function hands a closure to a REAL OS thread. It is the single source of truth for where a data race can enter a program: the checker holds such a closure's captures to Send and rejects unsynchronized mutable globals reached from its body. Declaring it here rather than hardcoding entry points is what stops the list drifting — it already had, and a spawn with no arm shipped a pointer into a dead frame to another thread.
@concurrent
Goes on: fn, method.
This function may run its closure argument on several threads at once (std/shard's parallelMap runs it on every worker). Every run shares the one environment, so the closure may not write or move out of its captures: a literal that does is rejected at the call, and so is a closure value whose body the call cannot see. A named fn always passes. Per-worker state belongs in parallelMapWith's states.
@parks
Goes on: fn, method, extern.
This function may park the current green task. The checker forbids an element view of a mutable global (a for-in binding, a slice, a & into an element) from being live across a call to it: while the task is parked another task can push to that global and free the buffer the view points into. Rooted at the extern that switches context (swapcontext) and declared on the public primitives; every caller inherits it transitively, so a wrapper nobody annotated is still seen.
@synchronized
Goes on: method.
This method's closure argument is a critical section: the primitive provides the mutual exclusion and the happens-before edge, so a global written inside it is not racing. Without it the canonical Once.run(...) one-shot init reports as the race it prevents. The scan stops at the boundary, so a new synchronization type only has to declare itself.
@unsafe
Goes on: fn, method.
Calling this function requires an unsafe block. For a routine whose contract the compiler cannot check (std/foreign's view constructors assert that a raw pointer really addresses len initialized elements), where every operation in the body is individually checkable and so no other rule would ever ask the CALLER to opt in. On a method it guards the raw-descriptor seams of std's owning handles (OwnedFd.fromRaw, rawFd()): the number names whatever reused it once the owner drops.
@mustUse
Goes on: fn, method, extern.
Discarding this routine's result is the unused-result warning, as it already is for an Option or Result. For a bool or an integer that encodes failure (arenaFree returns false on a stale handle; an extern returns a C error code), where nothing in the type says the value must be looked at. let _ = f() discards on purpose. The DO-178C and NASA safety profiles make the warning an error.
@externalLinkage
Goes on: fn.
Give this function external C linkage so a dlopen'd library can resolve it.
@records(…)
Goes on: fn, extern.
Describe what an extern writes so record/replay can capture it: one string per output buffer, @records("buf[ret]", "len[4]") (sizes: a param, ret, a byte count, *p for the count p points at, cstr). Under MILO_RECORD the call's return value, errno and those bytes are recorded; under MILO_REPLAY the call is not made and they are copied back. With no arguments, only the return value is recorded. An extern taking a pointer that has neither this nor a std catalog entry is a replay hole (replay-hole).
@replayHooked
Goes on: fn, method.
This function records its own extern calls through std/replay (std's sys* wrappers), so the compiler must not redirect them under record/replay. Also a file-level directive, @!replayHooked, for std/replay and the runtime beneath it.
@link(…)
Goes on: fn, extern.
Name the native library this extern is resolved from.
An unknown attribute is an error, never a silent no-op. So is an attribute on the wrong kind of declaration: @cLayout on a non-extern struct, @link on a regular fn, or @externalLinkage on an extern fn (which declares a function defined elsewhere, so there is no definition there to give linkage to).
Compile-time builtins
These appear where a value does, and evaluate while compiling.
| Builtin | What it does |
|---|---|
@embedFile(path) | Inlines a file's contents as a string at compile time |
@targetOs() | The OS being compiled for, as a string |
Both also work without the @, but warn (bare-embedfile, bare-targetos): the sigil is what tells a reader the call never happens at runtime.
@embedFile(path)
let html = @embedFile("index.html")The argument must be a string literal, and the path resolves relative to the file containing the call. Contents are read as raw bytes, so binary assets embed intact.
What comes back is an ordinary string, so the data is parsed with the same code that would parse it off disk — the only difference is that there is no disk, and no failure path to handle:
// Sitting next to this file: version.txt holding `1.4.2`, and pairs.tsv holding
// two tab-separated rows — `alpha bravo` and `charlie delta`.
fn main(): i32 {
let version = @embedFile("version.txt").trim()
print(version) // 1.4.2
for line in @embedFile("pairs.tsv").trim().split("\n") {
let cols = line.split("\t")
print(cols[0] + " -> " + cols[1]) // alpha -> bravo, charlie -> delta
}
return 0
}The parsing still runs at startup; only the reading moved to compile time. That is the usual reason to reach for it — a single-file binary that carries its own assets, with no install step and no path to get wrong at the customer site.
Web servers are where that pays off most, since the alternative is shipping a static directory next to the binary and keeping the two in sync. A handler returns the asset directly:
// Handler shapes from std/http; `index.html` and `public/style.css` are the reader's
// own asset files, so this fence illustrates rather than compiles.
pub fn homeHandler(ctx: &mut Context): Response {
return ctx.html(@embedFile("index.html"))
}
pub fn styleHandler(ctx: &mut Context): Response {
return Response.Status(200, "text/css; charset=utf-8", @embedFile("public/style.css"))
}examples/net/webserver.milo serves its home page this way. examples/net/weather/app.milo goes further and embeds an entire PWA — HTML, CSS, JS, service worker, PNG icons, and a US place-name index — so the deployable is one file with no asset root to configure. examples/net/termpair/server.milo embeds xterm.js the same way. Binary assets work because the contents are raw bytes, not text.
@targetOs()
A constant that is "darwin", "linux", or "windows":
let devNull = if @targetOs() == "windows" { "NUL" } else { "/dev/null" }Both arms of the if are type-checked, but the compiler evaluates the condition and keeps only the taken arm — the other is never lowered or code-generated. That means the dead branch may reference symbols that exist on no other platform, such as a Windows-only extern, without breaking the build anywhere else. The fold triggers on any statically known condition: @targetOs() compared with a string literal, and !, &&, || over such comparisons.
For a C declaration that differs by platform, prefer the stdlib filename split over @targetOs() — see C FFI.
Notes on attributes
@derive is covered with Traits, and @link with C FFI. @cSig, @cLayout and @cValue make the build check an extern declaration against the real C headers; the rules are in C FFI.
Keeping an imported function for C callers
@externalLinkage forces external linkage on a function the compiler would otherwise see as unreachable and drop. The only caller is outside what reachability analysis can see: a dlopen'd library resolving a symbol back against this executable, or a C program linking a Milo archive.
@externalLinkage
pub fn pluginEntry(): i32 { return 7 }The rule is about where the definition lives, not about pub. Functions in the file being compiled get external linkage already; a function reached only through an import is internal by default, so dead-code elimination is free to drop it. With build-lib:
// mathlib.milo, the file passed to build-lib. `./helpers` is the reader's own file,
// so this fence is illustrative rather than compiled.
from "./helpers" import { miloAdd }
pub fn miloGreet(): void { print("hello from milo") }// helpers.milo, reached only by import, so it needs @externalLinkage
@externalLinkage
pub fn miloAdd(a: i32, b: i32): i32 { return a + b }Drop the @externalLinkage and miloAdd vanishes from both the generated header and the archive, and the C side fails to link. miloGreet needs no annotation, being in the file that was built. Building and linking the archive is in C FFI.
Not annotations
Contract clauses — requires, ensures, invariant, decreases — are ordinary keywords in the declaration, not @ annotations, because they are type-checked expressions rather than instructions to the compiler. old(e), which names a parameter's entry value inside an ensures, is contract-only syntax for the same reason. See Contracts & proofs.