Skip to content

Keyword Reference

Every word the language reserves, with the form it takes and what it means.

This page is generated from the compiler by scripts/gen-lang-docs.ts, through bun run src/main.ts lang --json. It is the same text the language server shows when you hover a keyword in an editor, so the page, the hover and the compiler cannot disagree.

A soft keyword is reserved only where the grammar expects it and is an ordinary identifier everywhere else — a variable may be called from.

The snippets below show the form of each construct, with where a body would go, so they are not compiled as programs.

as

milo
let n = x as i64
let p = 0 as *u8

An explicit conversion, and it is total — every input has a defined result, no undefined behavior. Integer → integer truncates or extends by bit width and wraps silently, which is the deliberate opt-out from the default overflow trap: reach for checked* / saturating* when you need to detect or clamp instead. Float → integer saturates at the target's bounds, and NaN maps to 0.

0 as *T is how a null raw pointer is spelled, and it is the one pointer cast needing no unsafe.

An integer-repr enum converts out with as i32; converting in goes through tryFrom, which returns an Option because an arbitrary integer may name no variant.

break

milo
break

Exits the innermost enclosing while or for loop immediately.

continue

milo
continue

Skips the rest of the loop body and starts the next iteration of the innermost enclosing loop.

decreases

milo
decreases <integer expr>

Termination measure: must be non-negative and strictly fall across every self-recursive call, or every loop iteration.

On a function this is not optional bookkeeping — a self-recursive call is modelled by assuming that function's own ensures, which is induction, and induction over a recursion that may not terminate proves anything. Without a discharged measure such a proof is reported as conditional.

derive

Soft keyword: reserved only where the grammar expects it.

milo
@derive(Json, Eq, Clone)
struct Point { x: i32, y: i32 }

derive Show {
    fn show(self: &Self): string { … }
}

Two related spellings. @derive(Trait, …) on a struct or enum generates the trait impls from the shape of the type — Json for serialization, Eq for ==, Clone for .clone(), and so on. Eq and Clone are also derived automatically for plain structs; Clone skips Drop and @noCopy types, whose duplication would release a resource twice.

derive Trait { … } declares the template a @derive(Trait) expands to, so a user-written trait can be derivable too. The body is a template, not ordinary code: @fields repeats over the fields of the type being derived for.

Soft keyword — a legal identifier elsewhere.

else

milo
if cond { … } else { … }

The branch taken when the if condition is false; else if chains another test.

Also pairs with let … else { … }: a let-else binds a pattern into the enclosing scope, and runs the else block when the pattern does not match. That block must diverge (return / break / continue) — the compiler rejects one that could fall through to code where the binding does not exist.

ensures

milo
ensures <bool expr>

Postcondition. Holds at every return. result names the return value; old(e) names what e was at entry.

enum

milo
enum Shape {
    Circle(f64),
    Square(f64),
}

A sum type: a value is exactly one variant, and a payload is reachable only through a pattern that binds it. match over one must be exhaustive, so adding a variant turns every incomplete match into a compile error.

A payload-free enum can carry explicit integer values (enum Color { Red = 1, … }) and then convert with as i32 / tryFrom.

extern

milo
extern fn puts(s: *u8): i32
extern fn printf(fmt: *u8, ...): i32

Declares a function implemented outside Milo. There is no body: the C linker resolves the symbol, and nothing on the far side is checked for memory safety. A trailing ... declares it variadic — get the fixed arity wrong and the call is miscompiled on AArch64, silently.

A call needs no unsafe when every argument auto-coerces (scalar, &T, a Milo fn, string / [T; N]*T, a matching *T, or a by-value extern struct) and the return is scalar, void, or an extern struct. A pointer return has provenance the compiler cannot see, so it forces an unsafe block at every call.

@cSig / @cLayout on the declaration make the C compiler check this signature (and a struct's layout) against the real header, rather than trusting that the hand-written declaration matches.

false

milo
let ok: bool = false

The false bool literal. Conditions must be bool — no other type is falsy.

fn

milo
fn name(param: T): Ret { … }

Declares a function. Parameter types are mandatory and the return type follows the : — omit it and the function returns void.

fn name<T>(x: T): T adds type parameters; generics are monomorphized, so a generic call costs nothing at run time. A &T parameter is fed the value bare at the call site (f(x), never f(&x)): shared borrows are implicit and &x is not an expression. A &mut T argument is spelled f(&mut x); the bare form is the error implicit-mut-borrow.

for

milo
for x in items { … }
for i in 0..n { … }

Iterates a container, a range, or anything with an iterator. The loop variable binds by reference into the container — it does not copy the element and does not consume the container, so items is still usable after the loop.

The binding is immutable: to change elements in place, assign through the index (v[i] = …) or hand the container to a function taking a &mut [T] slice.

from

Soft keyword: reserved only where the grammar expects it.

milo
from "std/json" import { Json }

Introduces an import, naming the module the following import { … } list draws from.

A soft keyword — it is only special at the start of an import, so from stays usable as a field or parameter name (struct Edge { from: i32, to: i32 }).

if

milo
if cond { … } else if other { … } else { … }

Branches on a bool — no truthiness, so an integer or an Option will not do. The condition needs no parentheses; the braces are mandatory.

if let Pattern = expr { … } matches a single pattern and binds its payload for the body, which is how you take an Option/Result apart without a full match.

impl

milo
impl Point {
    fn len(self: &Point): f64 { … }
}

impl Show for Point { … }

Attaches methods to a type: inherent methods with impl Type, a trait implementation with impl Trait for Type. The receiver is spelled out as the first parameter (self: &Point or self: &mut Point); a method with no self is a static, called as Point.new().

impl blocks are never marked pub — an impl's visibility follows the type it implements.

import

milo
from "std/json" import { Json }
from "std/io" import { readFile as slurp }

Binds names from another file. Paths are std/<name> for the standard library, or a path relative to the importing file.

This is the only form: there is no glob import, and a bare import "path" is rejected. An import binds the name locally — it never re-exports, so pub on an import is not a thing.

in

Soft keyword: reserved only where the grammar expects it.

milo
for entry in dir { … }

Separates the loop variable from the sequence in a for loop. Also spells a range: for i in 0..n.

A soft keyword — it is only special in for position, so in remains usable as a parameter or field name.

interface

milo
interface Draw {
    fn draw(self: &Self): void
}

A structurally typed, dynamically dispatched interface: any type whose methods match satisfies it, with no declaration linking the two. A &Draw parameter accepts any of them, and the value passed is a fat pointer (data pointer + itable), so the call dispatches at run time.

v1 restrictions: methods must take self: &Self, interfaces take no type parameters, there is no interface inheritance and no downcast back to the concrete type. When you want static dispatch and generic bounds, use trait.

invariant

milo
invariant <bool expr>

On a loop: holds before every iteration. Proved by induction — established on entry, preserved by one pass through the body — and then available to everything after the loop.

On a struct (written after the closing brace, over bare field names): a property of the TYPE. Assumed wherever a value of that type is observed, and owed at every struct literal and every &mut function that could break it.

is

milo
if shape is Shape.Circle { … }

Tests which variant an enum value currently holds, as a bool. It only inspects — it binds no payload, so reaching a payload still needs match or if let.

let

milo
let x = 5
let x: i64 = 5

An immutable binding — assigned once, never reassigned. It lowers to an SSA register, not a stack slot.

Use var for something you need to mutate. Milo has no shadowing: re-declaring a name already in scope is a compile error, not a new binding.

match

milo
match shape {
    Shape.Circle(r) => { … }
    Shape.Square(w) => { … }
}

Pattern-matches a value. Matching an enum must be exhaustive: leave a variant out and the program does not compile, which is what makes adding a variant a safe, compiler-guided change. _ is the catch-all arm.

match is also an expression — every arm's value becomes the value of the match. Matching on a &T binds payloads by reference and does not move the scrutinee.

move

milo
let f = move || { print(name) }

Makes a closure capture by value, taking ownership of what it names instead of borrowing it. That is what lets the closure outlive the scope it was written in — required to hand it to spawn, a Promise, or anything stored past the current frame.

A borrowing closure is cheaper but is confined to the enclosing scope, and the checker rejects letting one escape.

mut

milo
fn bump(n: &mut i64): void

Marks a reference parameter as mutable: &mut T may write through the borrow, &T may only read.

mut appears only inside a reference type. A local is made mutable by declaring it var, not by writing mut. References are second-class — legal in parameter position only, never stored in a struct or returned — and the caller marks the mutation at the call site (bump(&mut n)); a shared &T argument is passed bare.

null

milo
let x: Option<i64> = null

The empty Optionnull is sugar for Option.None, not a null pointer. Safe Milo has no null pointers at all: absence is an Option, which the type checker forces you to open before use.

A null raw pointer, at an FFI boundary, is written 0 as *T.

pub

Soft keyword: reserved only where the grammar expects it.

milo
pub fn parse(s: string): Doc { … }

Exports the declaration. Declarations are file-private by default — without pub, a name is visible only inside its own file, and referencing it from another file is a compile error. The unit of privacy is the file, matching how imports already work.

Applies to top-level fn, struct, enum, trait, type, interface and globals. A pub struct exposes its fields, except those named with a leading _, which are private to the declaring file. Not applicable to impl (visibility follows the type) or import (which binds locally).

pub is about other Milo files; @externalLinkage is what makes a symbol visible to the C linker. pub is a soft keyword: away from a declaration it stays an ordinary identifier (var pub = 5).

requires

milo
requires <bool expr>

Precondition. Must hold at every call site — milo prove discharges it there, and a contract-checking build asserts it on entry.

return

milo
return value
return        // from a void function

Returns from the enclosing function.

Like every statement it ends at the newline: an expression on the next line is a new statement, not the returned value. Since nothing after it can run, a statement following return in the same block is an unreachable-code error.

struct

milo
struct Point {
    x: i32,
    y: i32,
}

A product type with named fields, stored inline. A struct whose fields are all Copy (scalars, raw pointers, arrays of those) copies on assignment; one that owns anything — a string, a Vec, a Heapmoves, and touching the source afterwards is a compile error. @noCopy forces that move tracking onto a struct that would otherwise copy, which is how an FFI handle stops being duplicable.

struct Pair<A, B> adds type parameters. @derive(Json, Eq, …) above the declaration generates the trait impls. A single-field struct is Milo's newtype — a distinct type, not an alias.

thread_local

Soft keyword: reserved only where the grammar expects it.

milo
thread_local var counter: i64 = 0

Gives a global per-thread storage: every thread gets its own independently initialized copy, so no synchronization is needed to touch it and no other thread can observe the writes.

A soft keyword — an ordinary identifier anywhere but in front of a global declaration.

trait

milo
trait Show {
    fn show(self: &Self): string
}

A nominal, statically dispatched interface: a type has the trait only where an impl Trait for Type says so. Traits are what generic bounds (fn p<T: Show>(x: T)), operator overloading and @derive are built on, and every call monomorphizes — no vtable.

For runtime polymorphism over mixed concrete types, use interface instead.

true

milo
let ok: bool = true

The true bool literal. Conditions must be bool — no other type is truthy.

type

milo
type Bytes = Vec<u8>

A type alias — a second spelling for an existing type, interchangeable with it everywhere.

It is not a new type: an alias will not stop you passing a UserId where an OrderId is meant. For that, declare a single-field struct (a newtype), which the checker keeps distinct.

unsafe

milo
unsafe {
    let p = malloc(64)
    let v = *p
}

Opens a block for the operations the compiler cannot verify: dereferencing or indexing a raw pointer, x.addrOf(), and extern calls whose return or arguments break the safe-coercion rule.

It does not turn checking off — everything else inside the block is checked exactly as usual; it marks the seam where you own the invariant. 0 as *T (a null pointer literal) needs no unsafe, and string.cstr() hands out a *u8 without one because the string stays alive in the caller's scope.

var

milo
var x: i64 = 5
x = 6

A mutable binding, stored in a stack slot (alloca). Reassignment and &mut borrowing both require var.

At file scope, var name: T = … declares a global; thread_local var gives it per-thread storage.

while

milo
while i < n { … }
while let Option.Some(line) = readLine(i) { … }

Loops while the condition is true. while let loops as long as the pattern keeps matching, binding the payload each iteration — the idiomatic way to drain a source that reports exhaustion with Option.None.

A loop may carry invariant clauses (proved by induction and then available after the loop) and a decreases measure that proves it terminates.