docs / getting-started

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

TypeSyntaxDescription
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")
TypeOrderAddRemoveInspect
Queue<T>First in, first outenqueue(item T)dequeue() T?peek() T?
Stack<T>Last in, first outpush(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 block
  • match — handle Some and None branches
  • val ?? 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}")
TraitMethodsReturns
Queryable<T>is_empty, first, last, contains, index_ofbool / T? / int?
Transformable<T>map, filter, flat_mapSelf
Foldable<T>reduce, any, all, count_where, sum, min, maxscalar / T?
Sequenced<T>take, skip, sorted_by, unique, reversed, zip, enumerateSelf

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")
FunctionBehaviour
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 valuesFailure detail
Structsexpected Point(x=3, y=4), got Point(x=1, y=2)
Enums with a payloadexpected Circle(4), got Circle(3)
Resultexpected Err(boom), got Ok(1)
Optionexpected None, got Some(1)
assert_nevalues 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}")
MemberReturnsBehaviour
Regex.compile(pattern String)Result<Regex, RegexError>Static. Compiles a pattern, reporting a syntax error as Err.
matches(text String)boolWhether 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)StringEvery match replaced with to.
Match.text() / start() / end()String / int / intThe 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:

MemberReturnsBehaviour
Json.parse(source String)Result<Json, JsonError>Static. Parses a document; the error carries a position.
to_string()StringSerializes 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}")
MethodReturns
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}")
MemberReturnsBehaviour
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()intNumber of command-line arguments.
Args().element_at(index int)StringOne argument; Args is Iterable<String>, so for..in works.
platform()StringFree 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")
MemberReturnsBehaviour
Duration.from_nanos / from_micros / from_millis / from_secondsDurationStatic constructors — the unit is named at the call site, never implied.
as_nanos() / as_micros() / as_millis() / as_seconds()intReads a Duration back in a chosen unit.
Clock().now()InstantA point in time.
Clock().sleep(d Duration)—Blocks for at least d.
Instant.elapsed(clock Clock)DurationTime 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 drop runs 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 out parameter must be a var. Passing a let is a compile error.
  • The same variable cannot appear twice as out in a single call (no aliasing through the back door).
  • Types must match exactly — no implicit coercion.
  • For small auto-copy types, out compiles 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 via self.field = value. Instantiation uses named arguments matching init parameters.
  • 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

ModifierAccessible from
publicEverywhere (default for methods)
protectedDeclaring class and all subclasses
privateDeclaring 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.
  • Self type — Use Self in 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

ModuleContents
system.ioprint, println, eprint, eprintln
system.stringString class with intrinsics
system.mathabs, min, max, pow, sqrt, floor, ceil, round, sin, cos, tan, log, exp, PI, E, INF
system.resultResult<T, E> with is_ok, is_err, unwrap_or
system.testingassert, assert_eq, assert_ne, assert_panics
system.collections.arrayArray methods + Queryable/Transformable/Foldable/Sequenced traits
system.collections.listList methods + Queryable/Transformable/Foldable/Sequenced traits
system.collections.mapMap methods + ad-hoc map/filter/reduce
system.collections.setSet methods + ad-hoc map/filter/reduce
system.collections.queueQueue<T> — enqueue, dequeue, peek
system.collections.stackStack<T> — push, pop, peek
system.textRegex, Match — compile, matches, find, find_all, replace
system.jsonJson, JsonError — parse, to_string, get, at, typed accessors
system.fsFs, FsError — files and directories
system.osEnv, Args, platform, exit
system.timeClock, 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.mi imports b.mi and b.mi imports a.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")
AttributeApplies toArgumentEffect
@non_exhaustiveEnum—The enum may gain variants, so a match on it outside the defining module needs a default arm.
@must_useEnum—The value may not be silently discarded at a call site.
@deprecatedFunction, class, enumRequiredCallers get a warning carrying the message — normally the replacement to use.
@testFunction—Marks the function for discovery by miri test.
@ignoreFunctionRequiredSkips the test and reports the reason. Requires @test.
@xfailFunctionRequiredThe 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
OptionEffect
--dir DIRDirectory to search. Defaults to the current directory.
--filter SUBSTRINGRuns only tests whose path::name contains the substring.
--format pretty|jsonHuman-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.

ToolHowWhat it reports
Shadow heapMIRI_HEAP_GUARD=1An 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 countMIRI_ALLOC_COUNT=1Exact allocation totals for the run, split by category, so a change in allocation behaviour is measurable rather than guessed at.
RC verifier--verify-mirA 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 gpu streams, channel-based concurrency, and additional GPU backends behind the same forall surface, starting with a native SPIR-V / Vulkan path

More resources:

Miri is evolving fast. Trait objects, async, and the native GPU backends are actively being designed. Join us on GitHub to shape the language!