Getting Started
This guide walks you through installing Miri, writing your first program, and exploring the core language features.
Installation
Miri is built with Rust, so you'll need the Rust toolchain installed. If you don't have it yet:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Then clone and build Miri:
git clone https://github.com/miri-lang/miri.git
cd miri
make build
The compiler binary will be at target/debug/miri. For optimized builds, use make release (binary at target/release/miri). You can add it to your PATH or run it directly.
Note: Miri is under active development. Build from the main branch for the
latest features.
Hello World
Create a file called hello.mi:
use system.io
println("Hello, World!")
Compile and run:
./target/release/miri run hello.mi
Miri uses indentation for blocks — no braces, no semicolons. The main function is the entry point
of every program. If the file doesn't contain a main function, the code will be wrapped in a
main function automatically.
Variables
Miri has two ways to declare variables:
let — Immutable bindings
let name = "Miri"
let age = 1
let pi = 3.14159
// name = "Other" // Error: cannot reassign immutable variable
var — Mutable bindings
var counter = 0
counter = counter + 1
counter += 1 // Shorthand works too
Types are inferred but can be explicitly annotated:
let x int = 42
let name string = "Miri"
let flag bool = true
Miri supports a full range of numeric types: i8, i16, i32,
i64, i128, u8, u16, u32, u64,
u128, f32, f64. The default int is i64 and
float is f64.
Functions
Functions are declared with fn. Parameter types are required. Parameters use name type syntax (no colon).
use system.io.{print}
// With explicit return
fn add(a int, b int) int
return a + b
// With implicit return
fn sub(a int, b int) int
a - b
// Inline body
fn greet(name string): print(f"Hello, {name}!")
fn main()
let sum = add(10, 20)
let diff = sub(10, 20)
greet("World")
print(f"Sum: {sum}")
print(f"Diff: {diff}")
Functions without a return type implicitly return void. Functions support named arguments at the call site.
Control Flow
if / else
fn classify(n int) string
if n > 0
return "positive"
else if n < 0
return "negative"
else
return "zero"
unless
The inverse of if — runs the block when the condition is false:
unless list.is_empty()
print(f"List has {list.len()} items")
Loops
// For loop with range
for i in 0..10
print(f"{i}")
// For loop over collection
for item in items
print(f"{item}")
// While loop
var n = 10
while n > 0
print(f"{n}")
n -= 1
// Infinite loop
forever
let input = read_line()
if input == "quit"
break
Miri supports while, until, do-while, forever, and for..in loops. All loop types support inline syntax with a colon: for x in 1..10: print(x).
Pattern Matching
Pattern matching is a core feature of Miri. It's exhaustive — the compiler checks that all cases are covered.
enum Direction
North
South
East
West
fn direction_name(d Direction) String
match d
Direction.North: "north"
Direction.South: "south"
Direction.East: "east"
Direction.West: "west"
Destructuring
enum Shape
Circle(int)
Rectangle(int)
fn area(s Shape) int
match s
Shape.Circle(r): r * r
Shape.Rectangle(side): side * side
Matching on values
fn describe_number(n int) string
match n
0
return "zero"
1
return "one"
_
return "something else"
Match supports or-patterns (2 | 3), guard clauses (x if x > 10), and wildcard (_).
Strings & Formatted Strings
Miri has built-in string support with f-string interpolation:
let name = "Miri"
let version = 1
// Regular string
let greeting = "Hello!"
// Formatted string (f-string) — embed expressions with {}
let msg = f"Welcome to {name} v{version}"
print(f"{name} is {version} year(s) old")
// Expressions inside f-strings
print(f"2 + 2 = {2 + 2}")
print(f"Name length: {name.length()}")
Structs
Structs are value types with named fields. Construct them using named arguments.
use system.io
struct User
name String
age int
fn greet(u User)
println(f"{u.name} is {u.age} years old")
fn main()
let u = User(name: "Alice", age: 30)
greet(u)
Structs can be passed to and returned from functions. Small structs (all primitive fields, <= 128 bytes) are auto-copied on assignment — no reference counting overhead.
Enums
Enums support variants with and without associated data. Pattern matching extracts the data from each variant.
use system.io
enum Result
Ok(int)
Error(String)
fn handle(r Result)
match r
Result.Ok(n): println(f"Success: {n}")
Result.Error(msg): println(f"Error: {msg}")
fn main()
let r = Result.Ok(42)
handle(r)
Enums can have simple tag variants (Red, Green) or carry data (Ok(int), Error(String)). Each variant can hold different types and numbers of fields.
Tuples
Tuples are fixed-size, heterogeneous collections accessed by index. They support destructuring in match expressions.
use system.io
let t = (10, "hello", true)
println(f"{t[0]}") // 10
println(f"{t[1]}") // hello
let pair = (3, 7)
let sum = match pair
(a, b): a + b
println(f"sum: {sum}")
Collections
Miri has four built-in collection types, each with a full method API and for..in iteration support.
use system.io
use system.collections.array
use system.collections.list
use system.collections.map
use system.collections.set
// Array — fixed size
let nums = [1, 2, 3]
println(f"first: {nums.first()}")
// List — dynamic, growable
var items = List([1, 2, 3])
items.push(4)
items.push(5)
println(f"list length: {items.length()}")
// Map — key-value pairs
var scores = {"Alice": 95, "Bob": 87}
scores["Carol"] = 91
println(f"Alice: {scores['Alice']}")
// Set — unique values
let tags = {1, 2, 3}
if 2 in tags
println("found 2 in set")
Collection types at a glance
| Type | Syntax | Description |
|---|---|---|
| Array | [T; N] | Fixed-size, stack-friendly |
| List | [T] | Dynamic, growable |
| Map | {K: V} | Key-value pairs |
| Set | {T} | Unique values, supports in operator |
Each collection requires its corresponding import: use system.collections.array, use system.collections.list, use system.collections.map, use system.collections.set.
Queue & Stack
Queue<T> and Stack<T> add the two classic ordering disciplines on top of the collections above. Neither is a new primitive: both are built by composition over an existing collection, and both implement Iterable<T>, Queryable<T> and Foldable<T>, so that whole surface — for..in, contains, index_of, first, last, is_empty, reduce, any, all, count_where, sum, min, max — comes for free from the traits rather than being reimplemented on each type.
use system.io
use system.collections.queue
use system.collections.stack
fn main()
// Queue<T> — first in, first out.
let jobs = Queue<String>()
jobs.enqueue("build")
jobs.enqueue("test")
jobs.enqueue("deploy")
println(f"{jobs.length()} queued")
match jobs.peek()
Some(next): println(f"next: {next}")
None: println("nothing queued")
// dequeue returns T? — draining an empty queue yields None, never a trap.
while jobs.length() > 0
match jobs.dequeue()
Some(job): println(f"running {job}")
None: println("empty")
// Stack<T> — last in, first out.
let undo = Stack<String>()
undo.push("type 'a'")
undo.push("type 'b'")
match undo.pop()
Some(step): println(f"undid: {step}")
None: println("nothing to undo")
// Both are built by composition over the existing collections, so the
// whole Iterable / Queryable / Foldable surface comes along for free.
println(f"{undo.length()} step(s) left")
| Type | Order | Add | Remove | Inspect |
|---|---|---|---|---|
Queue<T> | First in, first out | enqueue(item T) | dequeue() T? | peek() T? |
Stack<T> | Last in, first out | push(item T) | pop() T? | peek() T? |
Both also carry length() and element_at(index int). Note that removal and inspection return T?, not T: draining an empty queue yields None rather than trapping or handing back a sentinel value, so the empty case is one you handle in the type system like any other.
Neither implements Transformable, so map and filter are not available on a queue or a stack directly — iterate it, or build a List from it, when you need those.
Import them with use system.collections.queue and use system.collections.stack.
Option Types
Option types represent values that may or may not be present. Add ? to any type to make it optional. The compiler prevents using option values without checking first — eliminating null pointer errors at compile time.
use system.io
// Option type: might be absent
let x int? = None
let y int? = 42
// Unwrap with if let
if let Some(val) = y
println(f"y is {val}")
// Pattern matching
match x
Some(n): println(f"got {n}")
None: println("x is empty")
// Coalesce with ??
let safe = x ?? 0
println(f"safe: {safe}")
Three ways to handle option types:
if let Some(x) = val— unwrap and use in a blockmatch— handleSomeandNonebranchesval ?? default— coalesce to a default value
Result<T, E>
Fallible operations return Result<T, E> — an enum with two variants, Ok(T) and Err(E). The compiler enforces must_use semantics: ignoring a Result value without inspecting it is a compile error, so fallible APIs cannot be silently dropped.
use system.io
use system.result
fn divide(a int, b int) Result<int, String>
if b == 0
return Result.Err("division by zero")
return Result.Ok(a / b)
fn main()
match divide(10, 2)
Result.Ok(v): println(f"got {v}")
Result.Err(e): println(f"err: {e}")
// unwrap_or returns the Ok value or a fallback.
let safe = divide(10, 0).unwrap_or(-1)
println(f"{safe}")
// Predicates for quick inspection.
let r = divide(8, 4)
println(f"{r.is_ok()} {r.is_err()}")
Inspect a result with match for full extraction, or with the helpers is_ok(), is_err(), and unwrap_or(default) for quick paths. There is no unwrap() that panics on Err — Miri's standard library does not panic, so the failure path is always explicit at the call site.
system.math
The math module ships a single set of numeric primitives that works on both CPU and GPU kernels. Functions are imported as free names — no receiver object, no method-call syntax.
use system.io
use system.math as M
use system.math.{sqrt, pow, abs, min, max, sin, cos, floor, ceil, round}
fn main()
// Numeric primitives — single set, GPU-compatible.
println(f"{sqrt(2.0)}")
println(f"{pow(2.0, 10.0)}")
println(f"{abs(-3.0)}")
println(f"{min(5.0, 9.0)} {max(5.0, 9.0)}")
// Trigonometry.
println(f"{sin(0.0)} {cos(0.0)}")
// Rounding.
println(f"{floor(3.7)} {ceil(3.2)} {round(3.5)}")
// Constants — accessed through the aliased module.
println(f"{M.PI} {M.E}")
The current surface covers abs, min, max, pow, sqrt, floor, ceil, round, sin, cos, tan, log, exp, plus the constants PI, E, and INF. CPU calls lower to libm / Cranelift intrinsics. GPU calls lower to the target backend's built-in equivalents (tanh, atan2, step, clamp, and mix are available in kernels too) — same source, same answer.
Collection Traits
Collections share a four-trait taxonomy that breaks the classic "kitchen sink" iterator interface into focused, composable pieces. List<T> and Array<T, N> inherit the default method bodies on each trait, so the same fluent pipeline works across collection types.
use system.io
use system.collections.list
fn main()
let xs = List([1, 2, 3, 4, 5])
// Transformable: map, filter, flat_map.
let doubled = xs.map(fn(x int) int: x * 2)
let evens = xs.filter(fn(x int) bool: x % 2 == 0)
// Foldable: reduce, any, all, sum, min, max.
// sum/min/max return T? — None on empty, Some(value) otherwise.
let total = xs.sum() ?? 0
let any_big = xs.any(fn(x int) bool: x > 4)
let folded = xs.reduce(0, fn(acc int, x int) int: acc + x)
// Sequenced: take, skip, sorted_by, reversed, zip, enumerate.
let head = xs.take(3)
let tail = xs.skip(2)
let paired = List([1, 2, 3]).zip(List([10, 20, 30]))
let indexed = List(["a", "b", "c"]).enumerate()
// Queryable: is_empty, first, last, contains, index_of.
println(f"{xs.first() ?? 0} {xs.last() ?? 0}")
println(f"{xs.contains(3)} {xs.index_of(4)}")
println(f"{total} {folded} {any_big}")
| Trait | Methods | Returns |
|---|---|---|
Queryable<T> | is_empty, first, last, contains, index_of | bool / T? / int? |
Transformable<T> | map, filter, flat_map | Self |
Foldable<T> | reduce, any, all, count_where, sum, min, max | scalar / T? |
Sequenced<T> | take, skip, sorted_by, unique, reversed, zip, enumerate | Self |
sum, min, and max return T? — None on an empty collection, Some(value) otherwise. The standard library never traps on an empty input. Combine with ?? default for a one-shot fallback.
Map<K, V> and Set<T> keep ad-hoc map / filter / reduce methods — they don't fit the generic Self-returning shape of the traits until associated types and generic methods land in a future release.
Testing
system.testing provides four assertion intrinsics. Each is lowered directly by the compiler — the abort path is synthesized at the call site, embedding the source path:line in the failure message.
use system.io
use system.testing
fn add(a int, b int) int
a + b
fn main()
// Pass silently.
assert_eq(add(2, 3), 5)
assert(add(1, 1) == 2, "addition should be commutative")
assert_ne(add(2, 2), 5)
// A failing assertion aborts with:
// Runtime error: assertion failed at <path>:<line>: expected 6, got 5
// assert_eq(add(2, 3), 6)
// assert_panics catches Miri-level panic(...) calls.
assert_panics(fn(): panic("boom"), "boom")
| Function | Behaviour |
|---|---|
assert(cond, msg?) | Aborts when cond is false. |
assert_eq<T>(actual, expected, msg?) | Aborts when values differ. Supports int/float/bool/String + all int widths, plus enums, Option, Result, structs, and classes defining equals. |
assert_ne<T>(a, b, msg?) | Aborts when values are equal. |
assert_panics(f, expected?) | Runs f() and aborts unless it calls panic(...). |
A failed assertion aborts with Runtime error: assertion failed at <path>:<line>: <detail>. assert_panics uses a setjmp/longjmp catch frame so a Miri-level panic(...) inside the closure is caught and reported as a pass.
The detail is rendered structurally rather than as an address, so a failure names the values that actually differ:
| Compared values | Failure detail |
|---|---|
| Structs | expected Point(x=3, y=4), got Point(x=1, y=2) |
| Enums with a payload | expected Circle(4), got Circle(3) |
Result | expected Err(boom), got Ok(1) |
Option | expected None, got Some(1) |
assert_ne | values must differ, both were 5 |
The same rendering is available in ordinary string interpolation — an Option, a Result, or any enum you define can go straight into an f-string:
use system.io
use system.testing
enum Shape
Circle(int)
Square(int)
struct Point
x int
y int
fn missing() int?
None
fn load() Result<int, String>
Result.Err("no such key")
fn main()
// f-strings render Option, Result and any user enum by variant.
let found = Some(15)
let absent = missing()
println(f"{found}") // Some(15)
println(f"{absent}") // None
println(f"{load()}") // Err(no such key)
let shape = Shape.Circle(3)
println(f"{shape}") // Circle(3)
// assert_eq compares and diffs the same shapes, plus structs and any
// class defining equals:
// assert_eq(Point(x: 1, y: 2), Point(x: 3, y: 4))
// -> expected Point(x=3, y=4), got Point(x=1, y=2)
assert_eq(Point(x: 1, y: 2), Point(x: 1, y: 2))
assert_eq(shape, Shape.Circle(3))
Structural == underpins all of this: enum payloads and managed fields compare by value, not by pointer. Note that a struct renders in an assertion failure but is not itself interpolatable — put its fields in the f-string instead.
Type Aliases
Type aliases create semantic names for existing types. They are transparent to the type checker — fully interchangeable with the underlying type.
type ID is String
type Pair is (int, int)
type ScoreMap is {String: int}
system.text
system.text provides regular expressions. Regex.compile returns a Result<Regex, RegexError> rather than panicking, so a malformed pattern is a value you handle — the same discipline as every other fallible operation in the standard library.
use system.io
use system.text
fn main()
// Regex.compile reports a bad pattern as a value, never a panic.
match Regex.compile("[0-9]+")
Result.Ok(digits)
let has_digit = digits.matches("a1b2")
println(f"contains a digit: {has_digit}")
// find returns the first Match, or None.
match digits.find("port 8080, fallback 9090")
Some(m): println(f"{m.text()} at {m.start()}..{m.end()}")
None: println("no digits")
// find_all returns every non-overlapping match.
for m in digits.find_all("a1b2c3")
println(f"found {m.text()}")
println(digits.replace("a1b2c3", "#"))
Result.Err(e): println(f"bad pattern: {e}")
| Member | Returns | Behaviour |
|---|---|---|
Regex.compile(pattern String) | Result<Regex, RegexError> | Static. Compiles a pattern, reporting a syntax error as Err. |
matches(text String) | bool | Whether the pattern occurs anywhere in text. |
find(text String) | Match? | The first match, or None. |
find_all(text String) | [Match] | Every non-overlapping match, in order. |
replace(text String, to String) | String | Every match replaced with to. |
Match.text() / start() / end() | String / int / int | The matched text and its half-open byte range. |
A pattern that is fixed at compile time can be written as a regex literal — re"^\d+$", with optional trailing flags — which the compiler validates while it type-checks, turning an invalid pattern into a compile error instead of a runtime Err. String also gained split, join, to_int and to_float, and match arms accept string, float and regex predicates.
system.json
system.json is a recursive Json enum with a parser and serializer written in Miri itself — the compiler has no special knowledge of it. Parse failures carry the line and column where the document went wrong.
use system.io
use system.json
fn main()
let source = "{\"name\": \"miri\", \"stars\": 3, \"tags\": [\"gpu\", \"compiler\"]}"
// parse returns Result<Json, JsonError>; the error carries line and column.
match Json.parse(source)
Result.Ok(doc)
// get(key) and at(index) return Json?, so a missing field is a
// value you handle, not a crash.
match doc.get("name")
Some(field)
let name = field.as_string() ?? "<unnamed>"
println(f"name: {name}")
None: println("no name field")
match doc.get("stars")
Some(field)
let stars = field.as_int() ?? 0
println(f"stars: {stars}")
None: println("no stars field")
match doc.get("tags")
Some(tags)
match tags.at(0)
Some(first)
let tag = first.as_string() ?? "?"
println(f"first tag: {tag}")
None: println("no tags")
None: println("no tags field")
Result.Err(e): println(f"parse error: {e}")
The Json enum has six variants — Object, Array, Text, Number, Bool and Null — and you can match on them directly. Most code instead reaches for the accessors, each of which returns an option so a missing or mistyped field is a value rather than a crash:
| Member | Returns | Behaviour |
|---|---|---|
Json.parse(source String) | Result<Json, JsonError> | Static. Parses a document; the error carries a position. |
to_string() | String | Serializes back to JSON text. |
get(key String) | Json? | An object member, or None. |
at(index int) | Json? | An array element, or None. |
as_string() / as_int() / as_float() / as_bool() | String? / int? / float? / bool? | The scalar value when the variant matches, else None. |
Numbers keep their original lexeme, so a value written 1 stays 1 and no precision is lost on a round trip. Object members are stored in a map, so to_string() does not preserve the key order of the source document.
system.fs
system.fs reaches the filesystem through an Fs handle. The handle is the capability: a function that is not given one cannot read or write a file, so the ability to touch the disk is visible in a signature instead of ambient across the whole program.
use system.io
use system.fs
fn main()
// An Fs handle is the capability: a function without one cannot touch
// the filesystem at all.
let fs = Fs()
let path = "/tmp/miri-notes.txt"
match fs.write_file(path, "first line\n")
Result.Ok(written): println(f"wrote {written} bytes")
Result.Err(e): println(f"write failed: {e}")
match fs.append_file(path, "second line\n")
Result.Ok(written): println(f"appended {written} bytes")
Result.Err(e): println(f"append failed: {e}")
match fs.read_file(path)
Result.Ok(text): println(f"read {text.length()} chars")
Result.Err(e): println(f"read failed: {e}")
println(f"exists: {fs.exists(path)}")
match fs.delete(path)
Result.Ok(removed): println(f"deleted: {removed}")
Result.Err(e): println(f"delete failed: {e}")
| Method | Returns |
|---|---|
exists(path String) | bool |
read_file(path String) | Result<String, FsError> |
write_file(path String, contents String) | Result<int, FsError> — bytes written |
append_file(path String, contents String) | Result<int, FsError> — bytes appended |
list_dir(path String) | Result<[String], FsError> |
create_dir(path String) | Result<bool, FsError> |
delete(path String) | Result<bool, FsError> |
cwd() | Result<String, FsError> |
FsError distinguishes NotFound, PermissionDenied, AlreadyExists, NotADirectory, InvalidData and Other, each carrying a message. It is @non_exhaustive, so a match on it needs a default arm and stays compiling when a future release adds a case.
system.os
system.os covers the process environment, the command line, and the host platform. Env and Args follow the same capability rule as Fs.
use system.io
use system.os
fn main()
// platform() names the host: "macos", "linux" or "windows".
println(f"platform: {platform()}")
// Env is the capability for the process environment.
let env = Env()
match env.set("MIRI_GREETING", "hello")
Result.Ok(replaced): println(f"set (replaced an existing value: {replaced})")
Result.Err(e): println(f"could not set: {e}")
// get returns String? — an unset variable is None, not an empty string.
match env.get("MIRI_GREETING")
Some(value): println(f"MIRI_GREETING={value}")
None: println("MIRI_GREETING is unset")
// Args is Iterable<String> over the command line.
let args = Args()
println(f"{args.length()} argument(s)")
for arg in args
println(f" {arg}")
| Member | Returns | Behaviour |
|---|---|---|
Env().get(name String) | String? | An unset variable is None, distinct from a variable set to the empty string. |
Env().set(name String, value String) | Result<bool, EnvError> | true when an existing value was replaced. |
Args().length() | int | Number of command-line arguments. |
Args().element_at(index int) | String | One argument; Args is Iterable<String>, so for..in works. |
platform() | String | Free function naming the host: macos, linux or windows. |
exit(code int) | — | Ends the process with a status code. |
system.time
system.time splits time into three types so that a duration can never be confused with a point in time, and neither can be confused with a bare number. A Clock is the capability for reading the wall clock and for sleeping.
use system.io
use system.time
fn main()
// Clock is the capability for reading the wall clock and sleeping.
let clock = Clock()
// Duration is built through static constructors, never a bare number,
// so the unit is always visible at the call site.
let pause = Duration.from_millis(50)
println(f"pausing for {pause.as_millis()} ms")
let started = clock.now()
clock.sleep(pause)
// An Instant measures against the clock it was taken from.
let taken = started.elapsed(clock)
println(f"slept at least 50 ms: {taken.as_millis() >= 50}")
println(f"{Duration.from_seconds(2).as_millis()} ms in two seconds")
| Member | Returns | Behaviour |
|---|---|---|
Duration.from_nanos / from_micros / from_millis / from_seconds | Duration | Static constructors — the unit is named at the call site, never implied. |
as_nanos() / as_micros() / as_millis() / as_seconds() | int | Reads a Duration back in a chosen unit. |
Clock().now() | Instant | A point in time. |
Clock().sleep(d Duration) | — | Blocks for at least d. |
Instant.elapsed(clock Clock) | Duration | Time since the instant was taken. |
Those static constructors are what made Duration.from_millis(500) possible: static class members shipped as the language feature this module needed.
Memory Model
Miri's memory model is built on a single promise: you never write memory annotations. The compiler infers ownership, manages reference counts, and proves linear flows so it can elide the bookkeeping. The only memory-related concept that ever appears in your source code is the out keyword (covered below).
Under the hood, every value falls into one of three buckets:
- Auto-copy types — Primitives (
int,float,bool) and small all-primitive structs (≤ 128 bytes) are copied bitwise on assignment. Zero overhead, no reference counting at all. - Managed types — Strings, collections (
List,Map,Array,Set), and any struct or class with managed fields are tracked with reference counts. The compiler emits IncRef/DecRef instructions automatically — even for elements deep inside nested collections. - Resource types — Any type that defines a
fn drop(self)method. These are single-owner: aliasing is forbidden, and the compiler tracks them strictly so you cannot use one after passing it away.
Copy-on-Write: assignment shares, mutation forks
When you assign a managed value to a new variable, both bindings point at the same underlying buffer — there is no eager deep copy. The buffer's reference count goes up by one. The instant either side mutates the value, Miri silently forks: it copies the buffer, decrements the old RC, sets the new RC to 1, and applies the mutation to the fresh copy.
use system.io
use system.collections.list
fn main()
let a = List([1, 2, 3])
var b = a // No copy yet — both share the same buffer
b.push(4) // Mutation triggers Copy-on-Write
println(f"{a.length()} {b.length()}") // 3 4
// RC is incremented on share, decremented when each goes out of scope.
// The buffer is freed automatically when the last owner releases it.
The result is the best of both worlds: value semantics (mutating b never changes a), but with the performance of reference passing when no one mutates. CoW applies to List, Map, Array, Set, and String.
Zero-cost RC elision
The Perceus optimization pass analyses every function for linear flows. When the compiler proves that a value is created, used once, and discarded — never aliased, never escaping — it removes all of the IncRef/DecRef calls for that value entirely. You write idiomatic code. The compiler emits straight-line allocation and drop with no atomic counter traffic on the hot path.
Automatic recursive cleanup
When a managed value's RC reaches zero, Miri runs a compiler-generated destructor that, in order: (1) calls the user's fn drop(self) if one is defined, (2) recursively decrements every managed field, (3) frees the allocation. You never write .close(), .dispose(), or try/finally.
Cloneable & .clone()
Sometimes you want an independent deep copy up front rather than waiting for Copy-on-Write to fire. The Cloneable trait provides a .clone() method that does exactly that.
use system.io
use system.collections.list
fn main()
let a = List([1, 2, 3])
// .clone() forces an independent deep copy up front,
// skipping the share-then-CoW dance.
var b = a.clone()
b.push(4)
println(f"{a.length()} {b.length()}") // 3 4
// Strings, Maps, Arrays, and Sets are all Cloneable.
let greeting = "hello"
let copy = greeting.clone()
println(copy)
All managed types (String, List, Map, Array, Set) implicitly implement Cloneable. User-defined classes get an auto-generated __clone_TypeName helper — primitives are copied bitwise, managed fields are recursively cloned. Resource types (those with fn drop(self)) intentionally do not implement Cloneable: copying a file handle or a network socket is almost always a bug.
When to reach for .clone(): when you need to keep using a managed value after passing it to a function that consumes it (see Use-After-Move below), or when you want to break aliasing eagerly to avoid a CoW copy later in a tight loop.
Resource Types & fn drop(self)
Some values represent things in the outside world — open files, sockets, GPU buffers, lock guards — and need explicit cleanup. Miri's answer is the fn drop(self) method. Defining one on a class or struct turns it into a resource type:
use system.io
// A type with `fn drop(self)` is a *resource type*.
// Miri runs `drop` exactly once when the value's lifetime ends.
class FileHandle
private path String
fn init(p String)
self.path = p
println(f"opened {self.path}")
fn drop(self)
println(f"closed {self.path}")
fn main()
let f = FileHandle(p: "config.toml")
println("doing work...")
// f goes out of scope here — `drop` fires automatically.
// No manual .close() needed.
- Miri guarantees
dropruns exactly once when the value's lifetime ends. - Resource types are single-owner — they cannot be shared via reference counting and they do not participate in Copy-on-Write.
- Resource types are intentionally not
Cloneable— most external resources cannot be meaningfully duplicated. - Aliasing a resource (
var alias = original) consumes the original, just like passing it to a function.
This is how Miri delivers RAII without manual .close() calls and without exception-handling ceremony.
Use-After-Move & Escape Analysis
Once you pass a resource to a function, the compiler refuses to let you use that variable again. This is enforced statically — there is no runtime check, just a compile error.
use system.io
// Resource types (those defining `fn drop(self)`) are tracked strictly.
// Once you pass one to a function, you can't use it again.
class Connection
public id int
fn init(i int)
self.id = i
fn drop(self)
println(f"closing {self.id}")
fn archive(c Connection)
println(f"archiving {c.id}")
fn main()
let c = Connection(i: 1)
archive(c)
// archive(c) // compile error: 'c' was consumed by 'archive'
// and cannot be used again
// Need to call archive twice? Use `.clone()` to opt in to a copy
// (only available for Cloneable types — resource types are
// intentionally not Cloneable, because cloning a file handle is
// almost always a bug).
The rule has two layers:
- Resource types are tracked strictly at every scope. Pass one to a function, store it in a field, or alias it via assignment, and the original binding is marked consumed. Any subsequent use is a compile error with diagnostic
E0110. - Managed types are tracked at the top level (script-style code) and inside function bodies via escape analysis. The compiler examines each callee's body to see whether your argument is just being read (a borrow) or genuinely escapes — returned, stored on the heap, or captured into a returned closure. Reads do not consume. Escapes do. The diagnostic explains the multi-hop chain that led to the move.
The fix is always the same: .clone() the value before the consuming call. Because escape analysis is precise, you only need to clone at the spots that actually matter — the borrow-checker tax of "clone everywhere just in case" is gone.
The out Keyword
So far, every memory rule has been inferred. The one — and only — explicit memory concept in the language is out. Mark a parameter out to let a function modify a caller's variable in place.
use system.io
use system.collections.list
// `out` lets a function modify a caller's variable in place.
// The only memory-related keyword in the language.
fn inc(n out int)
n = n + 1
fn append_99(list out [int])
list.push(99)
fn main()
var x = 41
inc(x)
println(f"{x}") // 42
var items = List([1, 2])
append_99(items)
println(f"{items.length()}") // 3
// Passing a `let` binding to an `out` parameter is a compile error —
// out parameters always require a mutable variable.
Rules at a glance
- The argument passed to an
outparameter must be avar. Passing aletis a compile error. - The same variable cannot appear twice as
outin a single call (no aliasing through the back door). - Types must match exactly — no implicit coercion.
- For small auto-copy types,
outcompiles to a mutable reference (no allocation). For managed/large types, the value is moved in and moved back out — ownership transfers to the callee and returns to the caller.
That's the whole memory-keyword surface. No lifetimes, no &mut, no borrow-vs-move distinctions to memorize.
Classes
Classes are reference types with constructors, methods, visibility modifiers, and single inheritance. Method calls on base-typed variables are dispatched at runtime via vtables.
use system.io
abstract class Shape
abstract fn area() float
class Circle extends Shape
private radius float
fn init(r float)
self.radius = r
fn area() float
3.14159 * self.radius * self.radius
class Rectangle extends Shape
private width float
private height float
fn init(w float, h float)
self.width = w
self.height = h
fn area() float
self.width * self.height
fn main()
let c = Circle(r: 5.0)
println(f"{c.area()}") // 78.53975
// Virtual dispatch — method resolved at runtime
let s Shape = Circle(r: 3.0)
println(f"{s.area()}") // 28.27431
Key concepts
init— Constructor method. Fields are initialized viaself.field = value. Instantiation uses named arguments matchinginitparameters.extends— Single inheritance. Subclasses inherit all fields and methods.super.method()— Calls the parent class implementation.super.init()chains to the parent constructor.abstract— Abstract classes cannot be instantiated. Abstract methods must be overridden in concrete subclasses.- Virtual dispatch — When a variable is typed as a base class, method calls are resolved at runtime to the correct subclass implementation.
Visibility modifiers
| Modifier | Accessible from |
|---|---|
public | Everywhere (default for methods) |
protected | Declaring class and all subclasses |
private | Declaring class only |
Traits
Traits define shared interfaces — a set of abstract and optionally concrete method signatures that classes can implement. Traits support inheritance and default methods.
use system.io
trait Logger
fn prefix() String
"INFO" // default implementation
fn log(msg String)
println(f"[{self.prefix()}] {msg}")
class AppLogger implements Logger
fn prefix() String
"APP" // override default
fn main()
let logger = AppLogger()
logger.log("started") // [APP] started
Key concepts
implements— Attach one or more traits to a class. The class must provide implementations for all abstract methods.- Default methods — Traits can provide method bodies. Classes inherit the default unless they override it.
- Trait inheritance — Traits can extend other traits with
extends. Implementing a derived trait requires implementing the entire chain. - Multiple traits — A class can implement multiple traits:
class X implements A, B. - Combined — A class can extend a base class and implement traits:
class Fish extends Animal implements Swimmer. Selftype — UseSelfin trait signatures to refer to the implementing class's own type.
Closures
Lambdas are first-class values. They can be stored in variables, passed as arguments, and returned from functions. Closures capture variables from the enclosing scope by value.
use system.io
fn apply(f fn(int) int, x int) int
f(x)
fn main()
// Non-capturing lambda
let square = fn(x int) int: x * x
println(f"{square(5)}") // 25
// Capturing closure — captures `base` by value
var base = 100
let add = fn(n int) int: base + n
println(f"{add(42)}") // 142
// Passing closures as arguments
let double = fn(x int) int: x * 2
println(f"{apply(double, 7)}") // 14
At the ABI level, closures are represented as fat pointers (fn_ptr, env_ptr). Captured variables are copied into an environment struct at the point of closure creation.
Generics
Generic functions and types are monomorphized at compile time — a specialized copy is emitted for each unique set of type arguments. No runtime cost.
use system.io
// Generic function — monomorphized per type
fn identity<T>(x T) T
x
// Generic struct
struct Pair<T, U>
first T
second U
// Generic class
class Box<T>
private value T
fn init(v T)
self.value = v
fn get() T
self.value
fn main()
let n = identity(42)
let s = identity("hello")
let p = Pair<int, String>(first: 1, second: "one")
println(f"{p.first}: {p.second}") // 1: one
let b = Box<int>(v: 99)
println(f"{b.get()}") // 99
Calling identity(42) and identity("hello") produces two separate compiled functions (identity_int, identity_string). Generic structs and classes work the same way — each unique instantiation gets its own compiled type.
Imports & Modules
Miri supports a module system that resolves imports, enforces visibility across module boundaries, and detects errors like circular dependencies and namespace collisions.
Import syntax
// Import all public entities from a module
use system.io
// Selective import
use system.io.{print, println}
// Module aliasing
use system.collections.list as L
// Multiple aliases in one statement
use system.{io, collections.map as M}
// Import from local project files
use local.models.user
use local.utils.math
Standard library modules
| Module | Contents |
|---|---|
system.io | print, println, eprint, eprintln |
system.string | String class with intrinsics |
system.math | abs, min, max, pow, sqrt, floor, ceil, round, sin, cos, tan, log, exp, PI, E, INF |
system.result | Result<T, E> with is_ok, is_err, unwrap_or |
system.testing | assert, assert_eq, assert_ne, assert_panics |
system.collections.array | Array methods + Queryable/Transformable/Foldable/Sequenced traits |
system.collections.list | List methods + Queryable/Transformable/Foldable/Sequenced traits |
system.collections.map | Map methods + ad-hoc map/filter/reduce |
system.collections.set | Set methods + ad-hoc map/filter/reduce |
system.collections.queue | Queue<T> — enqueue, dequeue, peek |
system.collections.stack | Stack<T> — push, pop, peek |
system.text | Regex, Match — compile, matches, find, find_all, replace |
system.json | Json, JsonError — parse, to_string, get, at, typed accessors |
system.fs | Fs, FsError — files and directories |
system.os | Env, Args, platform, exit |
system.time | Clock, Instant, Duration |
Cross-module visibility
Visibility modifiers (public, private, protected) are enforced across module boundaries. Top-level functions and classes are public by default. Accessing a private symbol from another module produces a compile error.
// utils/helper.mi
public fn add(a int, b int) int
a + b
private fn internal_detail() int
42
// main.mi
use local.utils.helper
fn main()
let x = add(1, 2) // OK — add is public
// internal_detail() // Error — private to its module
Error detection
- Namespace collisions — Importing two modules that export the same name produces a compile error with suggestions for resolution (e.g., using aliased imports).
- Circular dependencies — If module
a.miimportsb.miandb.miimportsa.mi, the compiler reports the circular import chain with clear diagnostics.
Multi-File Projects
Programs can span multiple .mi files. The compiler discovers, parses, and links all files in a project automatically. Local files are imported using the local prefix, with the path mapping directly to the file system relative to the project root.
// models/user.mi
use system.io
class User
public name String
fn init(n String)
self.name = n
public fn greet()
println(f"Hello, {self.name}")
// main.mi
use local.models.user
fn main()
let u = User(n: "Alice")
u.greet()
Compile and run multi-file projects the same way — point the compiler at your entry file and it resolves all dependencies:
./target/release/miri run main.mi
Attributes
An attribute is written @name above a declaration, optionally with a string argument. Attributes are a closed set: the compiler knows every one it accepts and rejects anything else with an error. A misspelled attribute can therefore never be silently ignored, which is the failure mode that makes attributes untrustworthy in other languages — @tset is a compile error, not a test that quietly never runs.
use system.io
// @non_exhaustive: this enum may gain variants later, so a match on it
// outside the defining module must carry a default arm.
@non_exhaustive
public enum ConnectionError
Refused(String)
TimedOut(int)
// @must_use: the value may not be silently discarded at a call site.
@must_use
public enum Receipt
Written(int)
// @deprecated carries the replacement, and callers get a warning.
@deprecated("acquire a Clock and use clock.now()")
fn legacy_timestamp() int
0
fn main()
println("attributes are a closed set — an unknown one is a compile error")
| Attribute | Applies to | Argument | Effect |
|---|---|---|---|
@non_exhaustive | Enum | — | The enum may gain variants, so a match on it outside the defining module needs a default arm. |
@must_use | Enum | — | The value may not be silently discarded at a call site. |
@deprecated | Function, class, enum | Required | Callers get a warning carrying the message — normally the replacement to use. |
@test | Function | — | Marks the function for discovery by miri test. |
@ignore | Function | Required | Skips the test and reports the reason. Requires @test. |
@xfail | Function | Required | The test is expected to fail. Requires @test. |
Applying an attribute to the wrong kind of declaration, omitting a required argument, or passing one where none is accepted are all errors as well — as is using @ignore or @xfail without the @test they depend on.
The miri test Runner
miri test is a native test runner. It walks a directory tree, discovers every @test function, and runs each one in its own subprocess — so a test that segfaults is reported as a failure instead of taking the whole run down with it.
use system.testing
// `miri test` discovers every @test function in the directory tree and runs
// each one in its own subprocess, so a crash reports as a failure rather
// than taking the whole run down.
@test
fn addition_works()
assert_eq(2 + 2, 4)
@test
fn strings_concatenate()
assert_eq("mi" + "ri", "miri")
// @ignore skips the test and prints the reason. It requires @test.
@test
@ignore("needs a fixture file")
fn reads_the_config()
assert(false)
// @xfail pins a known bug: the test is expected to fail, and the run turns
// red if it starts passing — so a fixed bug cannot stay marked broken.
@test
@xfail("sub-word element stride, tracked upstream")
fn known_bug()
assert_eq(1, 2)
Run it against a directory:
miri test --dir .
which reports in the familiar cargo style:
running 4 tests
test docs-test-runner.mi::addition_works ... ok
test docs-test-runner.mi::strings_concatenate ... ok
test docs-test-runner.mi::reads_the_config ... ignored, needs a fixture file
test docs-test-runner.mi::known_bug ... ok (expected failure)
test result: ok. 3 passed; 0 failed; 1 ignored
| Option | Effect |
|---|---|
--dir DIR | Directory to search. Defaults to the current directory. |
--filter SUBSTRING | Runs only tests whose path::name contains the substring. |
--format pretty|json | Human-readable (default) or machine-readable output. |
The point of @xfail is that it refuses to rot. A known-broken test stays in the suite and stays red-free, but the moment the underlying bug is fixed the test starts passing and the run turns red with FAILED (unexpected pass) — so a fix cannot land while the bug is still marked broken. That is the opposite of commenting a test out, which is how known bugs normally become forgotten bugs.
Two file shapes are rejected rather than run: a file that declares its own main (it would collide with the dispatcher the runner synthesizes), and a file with executable statements outside any function (they would be silently dropped). A rejected file fails the run even when every test that did execute passed.
Memory Diagnostics
Miri manages memory for you, which means a reference-counting mistake is a compiler bug rather than yours. These tools exist to catch such bugs — they are how the standard library above was brought to a provably leak-free state, and running your own program under them costs you nothing but a rebuild.
| Tool | How | What it reports |
|---|---|---|
| Shadow heap | MIRI_HEAP_GUARD=1 | An ASan-style shadow heap that traps use-after-free and double-free at the moment they happen, and attributes each leak to the allocation site. Silent on a correct program. |
| Allocation count | MIRI_ALLOC_COUNT=1 | Exact allocation totals for the run, split by category, so a change in allocation behaviour is measurable rather than guessed at. |
| RC verifier | --verify-mir | A path-sensitive check of the reference-counting invariants after the Perceus transform. Reports any imbalance as a hard error. |
The verifier also honours the MIRI_VERIFY_MIR environment variable, which is how it runs across the compiler's own test suite on every change.
What's Next
You've covered the core language available in Miri v0.6.0-beta.4. The headline of this release is the
extended standard library: system.fs, system.os and
system.time for capability-scoped access to the outside world, regular expressions in
system.text, a pure-Miri JSON parser in system.json, and Queue and
Stack — together with a closed attribute set, the native miri test runner, and
memory diagnostics that keep the whole library provably leak-free.
Nothing in that library panics, and none of it hands back a sentinel: index_of returns
int? rather than -1, and pop and remove_at return
T? rather than trapping. The absent case is always a value you handle.
The GPU preview from the previous release is unchanged and still current: device-resident
bindings (gpu let / gpu var), forall kernels, on-device reduction,
shared memory, atomics, warp operations, vector types, and interactive gpu frame programs that
compile to the browser via WebGPU — with no shader files and no FFI.
Continue with the GPU Programming guide — it starts from your first kernel and assumes no GPU background — or see the programs running live in the GPU Playground.
Here's what's coming next:
- Future milestones — Trait objects, capture-by-reference closures, async/await and
async gpustreams, channel-based concurrency, and additional GPU backends behind the sameforallsurface, starting with a native SPIR-V / Vulkan path
More resources:
- Explore the test suite for more code examples
- Star the project on GitHub and follow development
- Check the issue tracker for the roadmap
Miri is evolving fast. Trait objects, async, and the native GPU backends are actively being designed. Join us on GitHub to shape the language!