docs / agents

Miri for Agents

Miri is built to be written by coding agents — Claude Code, Cursor, Codex and the like — with a person reviewing the result. The compiler explains every mistake in words an agent can act on, often hands it the exact fix, and answers lookup questions so the agent never has to guess a name. This guide shows how to set that up and how each tool works, with real output from the current compiler.

What Agents Get

An agent writing code does best when it can try something, see exactly what went wrong, and fix it without guessing. Miri gives it these tools, all in the miri command:

ToolWhat it does for the agent
The loopOne short routine — check, fix, test — that the agent repeats until the code is clean.
miri check --format jsonEvery error and warning as JSON: a stable code, the exact place, a one-line help, and often a ready-made repair.
miri fixApplies those repairs to the file in one call.
miri explainDescribes the rule behind any error code, with a before-and-after example.
miri viewAnswers “what can I call on this type?” and “what does this module offer?” from the real standard library.
miri testRuns @test functions and reports each failure with the expected and actual values.
miri patchEdits inside one function and refuses the edit if the file would stop compiling.
Skill packsShort guides the agent reads before writing Miri: the syntax, the traps, and the loop.

Today the compiler knows 189 diagnostic codes. 16 of them can come with an automatic repair, drawn from 19 kinds of repair. A code covers several situations and only some of them have a repair, so read those numbers as “at least”.

Why this matters: an agent that has to guess pays for every wrong guess with another round trip. When the compiler names the rule, points at the spot and offers the edit, most mistakes cost one step to fix.

Set Up in Two Commands

1. Install the compiler

You need the Rust toolchain and a C compiler (cc; on macOS run xcode-select --install, on Debian or Ubuntu apt install build-essential). Then:

git clone https://github.com/miri-lang/miri.git
cd miri
make install

make install builds the compiler and puts a miri link in ~/.local/bin. If that directory is not on your PATH yet, the command prints the line to add. Keep the clone where it is: the link points into it. To install somewhere else, pass BINDIR, for example make install BINDIR=/usr/local/bin.

2. Give your agent the skill packs

In the project your agent works on, run:

$ miri skill install
wrote ./.claude/skills/miri-lang/SKILL.md
wrote ./.claude/skills/miri-gpu/SKILL.md
wrote ./.claude/skills/miri-testing/SKILL.md
exit status 0

That is all for Claude Code: it finds skills in .claude/skills and opens the miri-lang pack whenever it works on .mi files. Other agents read a different folder, so name yours with --agent:

Your agentCommandWrites to
Claude Codemiri skill install.claude/skills/
Cursor, Codex, OpenCode, Windsurf, Gemini CLImiri skill install --agent agents.agents/skills/
Anything elsemiri skill install --agent genericskills/
$ miri skill install --agent agents
wrote ./.agents/skills/miri-lang/SKILL.md
wrote ./.agents/skills/miri-gpu/SKILL.md
wrote ./.agents/skills/miri-testing/SKILL.md
exit status 0

To see which packs this compiler carries:

$ miri skill list
miri-lang  Writing Miri source files — syntax, type system, and iteration workflow
miri-gpu  GPU kernels, forall loops, gpu fn, residency, and gpu frame blocks
miri-testing  Writing Miri tests — miri test runner, test attributes, and assertion functions
exit status 0

3. Check it works

Ask your agent for something small, like “write a Miri program that prints the sum of a list”. A set-up agent writes the file, runs miri check on it, and fixes what the compiler reports, without being told how.

Agent without skills support? Add one line to the instructions file it reads (AGENTS.md, CLAUDE.md, a rules file): “This project is written in Miri. Read skills/miri-lang/SKILL.md before editing .mi files, and run miri check FILE --format json after every edit.” Then install with --agent generic.

Updating

Pull, rebuild and refresh the packs:

cd miri && git pull && make install
cd your-project && miri skill install

Each installed pack records which compiler wrote it and a fingerprint of its text. A pack nobody has edited is updated in place. A pack you edited is left alone and reported (code MER_BLD_014); add --force to replace it anyway.

Without the compiler on hand

The packs are plain Markdown files in the compiler repository, so you can also copy one directly:

mkdir -p .claude/skills/miri-lang
curl -o .claude/skills/miri-lang/SKILL.md \
  https://raw.githubusercontent.com/miri-lang/miri/main/skills/miri-lang/SKILL.md

The agent still needs miri on its PATH to run the loop.

The Loop

There is one routine to follow, and the miri-lang pack gives it to the agent word for word. This is that section of the pack:

This is the one loop the compiler’s documentation recommends; everything else defers to it. Loop: miri check --format json → edit → miri test (or miri run). Iterate until clean.

  1. miri check — miri check myfile.mi --format json. Each diagnostic has a code, a help and, where one exists, a repair with its edits. A diagnostic without a repair is one you edit yourself.
  2. Edit — miri fix myfile.mi --apply --yes writes every recorded repair in one call. Everything else is an ordinary edit; in a file already in canonical layout, miri patch myfile.mi --replace-in-fn name --old text --new text makes a scoped edit and re-checks before anything reaches disk.
  3. miri test / miri run — miri test --dir <DIR>, miri run myfile.mi; both take --format json. Only a run finds a fault the frontend cannot see.

The loop, step by step

Here is a small program with two mistakes an agent coming from another language might make: && instead of and, and changing a value declared with let.

tally.mi
// Sums the scores between 5 and 10. Written with two mistakes for the compiler to repair.

use system.collections.list

fn main()
    let total = 0
    let scores = List([7, 9, 4])
    for s in scores
        if s > 5 && s < 10
            total = total + s
    println(f"total {total}")

Check. The compiler reports the first problem as JSON. Look for code, help and repair:

$ miri check tally.mi --format json
{
  "schemaVersion": 1,
  "ok": false,
  "command": "check",
  "diagnostics": [
    {
      "severity": "error",
      "code": "MER_PAR_020",
      "message": "'&&' is not a valid operator in Miri",
      "path": "tally.mi",
      "line": 9,
      "column": 18,
      "length": 2,
      "help": "Miri spells logical and as the word `and`: `if a and b`.",
      "fixSafety": "local-edit",
      "repair": {
        "id": "word-operator",
        "summary": "Write `&&` as `and`.",
        "edits": [
          {
            "path": "tally.mi",
            "start": 219,
            "end": 221,
            "replacement": "and"
          }
        ]
      },
      "related": []
    }
  ],
  "exitCode": 1,
  "durationMs": 0
}
exit status 1

Fix. The diagnostic carries a repair, so miri fix can write it:

$ miri fix tally.mi --apply --yes
Applied repairs to tally.mi
exit status 0

Check again. With the syntax fixed, the compiler can now see the next mistake, and it has a repair too:

$ miri check tally.mi --format json
{
  "schemaVersion": 1,
  "ok": false,
  "command": "check",
  "diagnostics": [
    {
      "severity": "error",
      "code": "MER_TYP_042",
      "message": "Cannot assign to immutable variable 'total'",
      "path": "tally.mi",
      "line": 10,
      "column": 13,
      "length": 17,
      "fixSafety": "local-edit",
      "repair": {
        "id": "let-to-var",
        "summary": "Declare the variable with `var` so it can be reassigned.",
        "edits": [
          {
            "path": "tally.mi",
            "start": 135,
            "end": 138,
            "replacement": "var"
          }
        ]
      },
      "related": []
    }
  ],
  "exitCode": 1,
  "durationMs": 23
}
exit status 1

Fix, check, run.

$ miri fix tally.mi --apply --yes
Applied repairs to tally.mi
exit status 0
$ miri check tally.mi --format json
{
  "schemaVersion": 1,
  "ok": true,
  "command": "check",
  "diagnostics": [],
  "exitCode": 0,
  "durationMs": 18
}
exit status 0
$ miri run tally.mi
total 16
exit status 0

Some problems hide others: a syntax error stops the compiler before it checks types. That is why the loop is a loop. Fix what is reported, check again, and stop when ok is true.

Reading Diagnostics

Without --format json, miri check prints errors for people:

$ miri check tally.mi
error[MER_PAR_020]: Unsupported C-Style Operator
--> tally.mi:9:18
   |
 9 |         if s > 5 && s < 10
   |                  ^^ '&&' is not a valid operator in Miri
   |
  = help: Miri spells logical and as the word `and`: `if a and b`.
   |
exit status 1

With --format json it prints the same information as one JSON object on standard output. Every command that takes --format json — check, build, run, test, fix, explain, view, patch, skill — answers in this same shape, so an agent only learns to read it once.

The fields an agent needs most:

  • ok — whether the command did what was asked. Warnings never make it false.
  • exitCode — the same number the process exits with.
  • diagnostics — one entry per problem, each with:
    • code — a stable name such as MER_TYP_042. Pass it to miri explain for the full rule.
    • line, column and length — where the problem is, counted in characters from 1.
    • help — one sentence on what to do instead.
    • repair — when present, the exact edits that fix it: byte offsets start and end in the file, and the replacement text. miri fix applies them for you, or an agent can apply them itself.
    • fixSafety — how safe the fix is to apply without a person looking (see Repairs).

Every field is listed in the JSON reference.

Repairs — miri fix

A repair is an edit the compiler is sure of. When a diagnostic carries one, miri fix can write it. Without --apply it only shows what it would do:

$ miri fix tally.mi
tally.mi:9:18 [MER_PAR_020]
  '&&' is not a valid operator in Miri
  repair word-operator: Write `&&` as `and`.
    replace `&&` with `and` at 219
exit status 0

miri fix FILE --apply --yes writes every repair in the file in one go. (--yes confirms the write when there is no terminal to ask at, which is always the case for an agent.)

How safe is a repair?

Every diagnostic code has a fixSafety level. miri fix --apply writes the first three on its own and refuses the rest unless you add --allow-risky:

LevelMeaningApplied by default
format-onlyChanges layout only.yes
behavior-preservingChanges the text, not what the program does.yes
local-editChanges one spot, and nothing outside it can notice.yes
api-changingChanges something other code relies on, such as a parameter name callers can pass by name.no
target-changingChanges where or how the program runs, such as moving data to the GPU or narrowing a number.no
requires-human-reviewPlausible, but a person should look first.no

Every repair

The list the miri-lang pack carries, with the diagnostic codes that can offer each repair:

RepairWhat it doesOffered with
add-importImport a name that resolves in exactly one module.MER_TYP_034 MER_TYP_043
arrow-return-typeDrop the -> before a return type.MER_PAR_001
cast-to-slot-typeConvert a number with as to the width of the place it is stored; a value outside that width’s range is truncated or rounded.MER_TYP_002
colon-annotationDrop the : before a type annotation.MER_PAR_001
concat-to-formatted-stringRewrite a + chain joining text to values as one f-string.MER_TYP_002
drop-extra-argumentsDrop positional arguments a call does not declare.MER_TYP_030
drop-iterator-accessorDrop a keys accessor on a keyed collection and iterate it directly.MER_TYP_033
if-colonWrite : in place of then, or after an inline else.MER_PAR_001
interpolation-quotesWrite the string literals inside an f-string’s braces with the other quote.MER_LEX_012
let-mut-to-varRewrite a let mut binding as var.MER_PAR_001
let-to-varRebind an immutable declaration as mutable.MER_TYP_042
move-into-mainMove statements written beside main into it, in order, ahead of what it already does.MER_TYP_069
null-to-noneRewrite null, nil or nullptr as None.MER_TYP_034
println-bangDrop the ! from a macro-style call.MER_LEX_001
qualify-variant-patternPrefix a bare variant pattern with the enum that declares it.MER_TYP_038
remove-unused-importDelete a use line nothing reads, or the unread names in a selection.MER_IMP_005
rename-to-suggestionRewrite a name as the one spelling the help offers in its place.MER_IMP_003 MER_TYP_033 MER_TYP_034 MER_TYP_038 MER_TYP_043
underscore-unread-bindingPrefix a binding nothing reads with _ to say the value is not meant to be read.MER_TYP_071 MER_TYP_072
word-operatorRewrite !, && or || as not, and or or.MER_LEX_001 MER_PAR_020

Rules — miri explain

Every code has a page that says what the rule is, why it exists and how a fixed program looks:

$ miri explain MER_PAR_020
MER_PAR_020 Unsupported C-Style Operator
error

Rule
A logical operator has been spelled with symbols. Miri spells logical and as the word `and` and logical or as the word `or`; `&&` and `||` are not operators. `miri fix` rewrites them (`word-operator`).

Before
let a = true
let b = false
let both = a && b

After
let a = true
let b = false
let both = a and b

Reference
Parser (docs/reference/parser.md)

The parser consumes a token stream and produces an abstract syntax tree (AST). It enforces syntactic structure: balanced parentheses and braces, well-formed declarations and expressions, and correct statement ordering. The parser is indentation-sensitive and requires expressions and statements to respect Miri's syntax rules.
exit status 0

miri explain --list lists every code. Add --format json to get each code's title, severity, safety level and repairs as data.

Lookups — miri view

Agents often guess method names from other languages: .len(), .to_string(), .keys(). miri view answers from the real standard library instead, so one lookup replaces a string of wrong guesses.

What can I call on a type? Members a type gets from its traits are listed too:

$ miri view --type String --public
class String implements Equatable, Hashable, Comparable, Addable, Multiplicable, Iterable, Cloneable
    fn clone() String
    fn code_at(index int) int?
    fn compare(other String) int
    fn concat(other String) String
    fn contains(needle String) bool
    fn element_at(index int) String
    fn ends_with(suffix String) bool
    fn equals(other String) bool
    static fn from_code_point(code int) String?
    fn hash() int
    fn init() String
    fn is_empty() bool
    fn join(parts [String]) String
    fn length() int
    fn repeat(count int) String
    fn replace(old String, to String) String
    fn size() int
    fn split(separator String) [String]
    fn starts_with(prefix String) bool
    fn substring(start int, end int) String
    fn to_float() float?
    fn to_int() int?
    fn to_lower() String
    fn to_upper() String
    fn trim() String
    fn trim_end() String
    fn trim_start() String
exit status 0

What does a module offer?

$ miri view system.fs --outline --public
use system.result
use system.string
// Represents a filesystem operation error.
@non_exhaustive
public enum FsError
    NotFound(String)
    PermissionDenied(String)
    AlreadyExists(String)
    NotADirectory(String)
    InvalidData(String)
    Other(String)
// Filesystem capability class. Acquire a handle with `let fs = Fs()` to access filesystem operations.
public class Fs
    // Checks whether a file or directory exists at the given path.
    public fn exists(path String) bool
    // Reads the entire contents of a file into a string.
    public fn read_file(path String) Result<String, FsError>
    // Writes the given contents to a file, truncating if it exists.
    public fn write_file(path String, contents String) Result<int, FsError>
    // Appends the given contents to a file, creating it if it doesn't exist.
    public fn append_file(path String, contents String) Result<int, FsError>
    // Lists the entries (file and directory names) in a directory.
    public fn list_dir(path String) Result<[String], FsError>
    // Creates a directory at the given path, creating missing parents as needed.
    public fn create_dir(path String) Result<bool, FsError>
    // Deletes a regular file at the given path.
    public fn delete(path String) Result<bool, FsError>
    // Returns the current working directory as an absolute path.
    public fn cwd() Result<String, FsError>
exit status 0

Show me one function, from a module or from a file of your own:

$ miri view scores.mi --fn total_of_passing
fn total_of_passing(scores [int]) int
    var total = 0
    for s in scores
        if s >= 5: total = total + s
    total
exit status 0

miri view --stdlib-root prints where the standard library is searched for. Set MIRI_STDLIB_PATH if the compiler cannot find it.

Tests — miri test

Mark a function with @test and miri test finds and runs it. Each test runs in its own process, so a crash fails one test instead of the whole run. Here is a file with a deliberately wrong expectation in its second test:

scores.mi
// A function and two tests for it; the second test expects the wrong total.

use system.collections.list
use system.testing.{assert_eq}

fn total_of_passing(scores [int]) int
    var total = 0
    for s in scores
        if s >= 5: total = total + s
    total

@test fn counts_scores_from_five_up()
    assert_eq(total_of_passing(List([7, 5, 4])), 12)

@test fn empty_list_totals_zero()
    assert_eq(total_of_passing(List<int>()), 1)
$ miri test scores.mi
running 2 tests
test scores.mi::counts_scores_from_five_up ... ok
test scores.mi::empty_list_totals_zero ... FAILED

failures:

---- scores.mi::empty_list_totals_zero ----
Runtime error: assertion failed at scores.mi:16:5: expected 1, got 0

test result: FAILED. 1 passed; 1 failed; 0 ignored
exit status 1

With --format json, each failure carries the code, the line and the expected and actual values, ready for the agent to compare:

$ miri test scores.mi --format json
{
  "schemaVersion": 1,
  "ok": false,
  "command": "test",
  "diagnostics": [],
  "exitCode": 1,
  "durationMs": 422,
  "tests": {
    "total": 2,
    "passed": 1,
    "failed": 1,
    "ignored": 0,
    "results": [
      {
        "path": "scores.mi",
        "name": "counts_scores_from_five_up",
        "outcome": "passed"
      },
      {
        "path": "scores.mi",
        "name": "empty_list_totals_zero",
        "outcome": "failed",
        "detail": "Runtime error: assertion failed at scores.mi:16:5: expected 1, got 0\n",
        "code": "MER_RT_005",
        "line": 16,
        "column": 5,
        "expected": "1",
        "actual": "0"
      }
    ],
    "rejectedFiles": [],
    "filesRead": 1
  }
}
exit status 1

The exit status says what happened: 0 all passed, 1 a test failed, 2 no test ran at all (a file did not compile, or there were no tests). An agent can trust a 0: a run that found nothing to test is never reported as a pass. The miri-testing pack covers assertions and test attributes.

Scoped Edits — miri patch

miri patch replaces text inside one named function, then checks the whole file before saving. A good edit is written:

$ miri patch scores.mi --replace-in-fn total_of_passing --old 's >= 5' --new 's > 5'
Applied 1 edit(s). The edited program checks.
exit status 0

An edit that would break the file is refused, and the file is left exactly as it was:

$ miri patch scores.mi --replace-in-fn total_of_passing --old 'total + s' --new 'total + s +'
error[MER_PAR_001]: Unexpected Token
--> scores.mi:10:3
    |
 10 |     total
    |   ^ Expected an expression, but found end of expression
    |
exit status 1

The text to replace must appear exactly once inside the function. patch works on files in Miri's standard layout, so run miri fmt FILE first if the file was written by hand. Ordinary file edits work just as well; patch is there for when you want the check before anything is saved.

For Editors — miri agent

An agent with a shell should use the commands above. miri agent is for tools that want to keep one compiler running instead of starting it for every command — an editor plugin, or a service with no shell. It speaks JSON-RPC 2.0 over standard input and output, framed with a Content-Length header the way a language server is:

→ {"jsonrpc":"2.0","id":1,"method":"check","params":{"path":"main.mi"}}
← {"jsonrpc":"2.0","id":1,"result":{"schemaVersion":1,"ok":false,"command":"check","diagnostics":[…]}}

→ {"jsonrpc":"2.0","id":2,"method":"fixApply","params":{"path":"main.mi"}}
← {"jsonrpc":"2.0","id":2,"result":{"schemaVersion":1,"ok":true,"command":"fix","diagnostics":[…]}}

Each method answers with exactly the JSON its command prints. The methods are initialize, check, explain, fixApply, view, patch and skillsGet, and $/cancelRequest cancels one in flight. The full protocol is in docs/agent-protocol.md, and tools/agent_client.py is a small working client in Python.

The Skill Packs

A skill pack is a short Markdown file in the Agent Skills format: a name, a one-line description that tells the agent when to open it, and the guide itself. They ship inside the compiler, so the packs you install always match the compiler you run. Below is the full text of each, exactly as an agent reads it.

miri-lang

Writing Miri source files — syntax, type system, and iteration workflow. View on GitHub ↗

Show the full pack
---
name: miri-lang
description: Writing Miri source files — syntax, type system, and iteration workflow
---

# Miri Language Essentials

The short core for writing `.mi` files: how Miri differs from Python and Rust,
the traps that cost earlier agents the most, and the one check → fix → test
loop. Everything else is a lookup, not a read: ask the compiler (see *Looking
things up*) rather than guessing a name.

## Core Syntax

Indentation-sensitive blocks; a `:` puts a one-line body on the header line.

```miri
use system.collections.list
use system.collections.map

struct Point
    x int
    y int

enum Color
    Red
    Blue

trait Drawable
    fn draw()

class Circle implements Drawable
    fn draw(): println("circle")

fn add(a int, b int) int
    a + b

fn main()
    let x int = 5
    var y = 10
    y = add(x, y)
    let p = Point(3, 4)
    var words = Map<String, int>()
    words["a"] = 1
    let items = List([1, 2, 3])
    for item in items
        if item > 1 and not (item == 3): println(f"{item} {p.x}")
    match Color.Red
        Color.Red: println("red")
        Color.Blue: println("blue")
    match Result.Ok(y)
        Result.Ok(v): println(f"ok {v}")
        Result.Err: println("err")
    let maybe int? = words.get("a")
    match maybe
        Some(v) if v > 0: println(f"some {v}")
        Some(_) | None: println("none")
```

## Where Miri Differs

- Bindings are `let` (immutable) and `var` (mutable); there is no `let mut`.
  Parameters are immutable too; mutate a collection argument with `.set(i, v)`.
- Types follow the name with a space: `let x int = 5`, `fn f(a int) int`. No `:`
  before a type, no `->`, no literal suffixes (`42i32`); cast with `as`.
- Logic is `and`, `or`, `not`. Nullable is `T?` with bare `Some`/`None`;
  `Result` patterns need the prefix: `Result.Ok(v)`, `Result.Err`.
- `List([1, 2])` builds from a literal, `List<int>()` is empty, `Map<K, V>()` and
  `Set<T>()` likewise. `[1, 2, 3]` alone is a fixed `Array`; `{}` is a `Map`.
- A `for` over a `Map` walks its keys; there is no `.keys()`.
- An f-string is the only int → String conversion: `f"{n}"`, never `n.to_string()`
  or `"n=" + n`.
- `main() int` returns an exit status; `panic(message)` stops with status 1.
  There is no `exit`.
- Stdlib modules are imported by name (`use system.collections.list`).

```miri,fails=MER_PAR_001,expects-message=Expected an expression, but found :
let x: int = 5
```

```miri,fails=MER_TYP_038,expects-message=Expected enum variant pattern like Result.Ok
fn main()
    match Result.Ok(5)
        Ok(x): x
        Err: 0
```

```miri,fails=MER_TYP_036,expects-message=Cannot instantiate generic class 'List<T>' without explicit type arguments
use system.collections.list

fn main()
    let l = List()
```

## Traps

- **Quotes inside f-string braces** must be the other quote character:
  `f"{m['a']}"`. The same quote ends the string (MER_LEX_012).
- **Positions count characters**, in `length()`, `substring`, `element_at`,
  `code_at` and regex matches alike; only `size()` counts bytes. A `substring`
  range outside the text stops the program (or fails to compile, if constant).
- **Integer overflow stops the program** (`MER_RT_003`) instead of wrapping.
  Where wrapping is meant — a hash, a checksum — write `wrapping_add`,
  `wrapping_sub` or `wrapping_mul` from `system.math`.
- **Walk with `for`, not by position.** `for ch in s` and `for k in m` are one
  pass; indexing a `String`, `Map` or `Set` by a counter repeats lookups.
- **Every statement lives in a function.** A statement beside `main` never runs
  and is refused (MER_TYP_069); put the script's body inside `main`.
- **Integer widths do not convert implicitly.** An `int` value stored into an
  `i32`/`u32` slot, element or field needs `as i32`; the same holds between
  `float` and `f32`. `miri fix --apply` writes the cast.
- **Format specs follow Python**: `f"{x:.2f}"`, `f"{n:>8}"`, `f"{n:08x}"`. A
  precision needs `f` (`{x:.2}` is refused); `x`/`b`/`o` take integers only.
- **Imports are explicit.** A name from another module needs its `use` line;
  `miri fix main.mi --apply --yes` adds one that resolves in exactly one module.

## Looking things up

Ask before guessing a member, a module or a signature. These are cheap and exact:

- `miri view --type String --public` lists what can be called on a type,
  inherited and trait-supplied members included (`List`, `Map`, `int` alike).
- `miri view system.io --outline --public` lists what a module offers; try
  `system.fs`, `system.os`, `system.collections.map` the same way.
- `miri view system.fs --fn read_file` prints one function.
- `miri view --stdlib-root` prints the stdlib roots searched; set
  `MIRI_STDLIB_PATH` when the binary is not beside its `stdlib/`.
- `miri explain MER_TYP_033` describes the rule behind a diagnostic code.

The `miri-testing` pack covers `miri test`; the `miri-gpu` pack covers kernels.

## Verification Loop

This is the one loop the compiler's documentation recommends; everything else defers to it. **Loop:** `miri check --format json` → edit → `miri test` (or `miri run`). Iterate until clean.

1. **miri check** — `miri check myfile.mi --format json`. Each diagnostic has a `code`, a `help` and, where one exists, a `repair` with its edits. A diagnostic without a `repair` is one you edit yourself.
2. **Edit** — `miri fix myfile.mi --apply --yes` writes every recorded repair in one call. Everything else is an ordinary edit; in a file already in canonical layout, `miri patch myfile.mi --replace-in-fn name --old text --new text` makes a scoped edit and re-checks before anything reaches disk.
3. **miri test** / **miri run** — `miri test --dir <DIR>`, `miri run myfile.mi`; both take `--format json`. Only a run finds a fault the frontend cannot see.

The auto-applicable repairs are:
- `add-import`: Import a name that resolves in exactly one module.
- `arrow-return-type`: Drop the `->` before a return type.
- `cast-to-slot-type`: Convert a number with `as` to the width of the place it is stored; a value outside that width's range is truncated or rounded.
- `colon-annotation`: Drop the `:` before a type annotation.
- `concat-to-formatted-string`: Rewrite a `+` chain joining text to values as one f-string.
- `drop-extra-arguments`: Drop positional arguments a call does not declare.
- `drop-iterator-accessor`: Drop a `keys` accessor on a keyed collection and iterate it directly.
- `if-colon`: Write `:` in place of `then`, or after an inline `else`.
- `interpolation-quotes`: Write the string literals inside an f-string's braces with the other quote.
- `let-mut-to-var`: Rewrite a `let mut` binding as `var`.
- `let-to-var`: Rebind an immutable declaration as mutable.
- `move-into-main`: Move statements written beside `main` into it, in order, ahead of what it already does.
- `null-to-none`: Rewrite `null`, `nil` or `nullptr` as `None`.
- `println-bang`: Drop the `!` from a macro-style call.
- `qualify-variant-pattern`: Prefix a bare variant pattern with the enum that declares it.
- `remove-unused-import`: Delete a `use` line nothing reads, or the unread names in a selection.
- `rename-to-suggestion`: Rewrite a name as the one spelling the help offers in its place.
- `underscore-unread-binding`: Prefix a binding nothing reads with `_` to say the value is not meant to be read.
- `word-operator`: Rewrite `!`, `&&` or `||` as `not`, `and` or `or`.

miri-testing

Writing Miri tests — miri test runner, test attributes, and assertion functions. View on GitHub ↗

Show the full pack
---
name: miri-testing
description: Writing Miri tests — miri test runner, test attributes, and assertion functions
---

# Testing in Miri

Miri includes a built-in test runner and assertion library. This skill teaches how to write tests, run them, and understand test attributes.

## Writing and Running Tests

### Basic Test

Mark a function with `@test` to make it a test case:

```miri
use system.testing.{assert_eq}

@test fn simple_add()
    assert_eq(2 + 2, 4)
```

Run tests with the `miri test` command:

```bash
miri test --dir <directory>
```

The runner discovers all `.mi` files, finds functions marked with `@test`, and executes them.

### Assertion Functions

Miri provides four assertion functions (all require an import):

```miri
use system.testing.{assert, assert_eq, assert_ne, assert_panics}

@test fn test_assert()
    assert(true)

@test fn test_equals()
    assert_eq(5, 5)

@test fn test_not_equals()
    assert_ne(3, 4)

@test fn test_panic()
    assert_panics(fn(): panic("expected error"))
```

All assertions accept an optional message argument.

### Test Runner Options

```bash
miri test --dir <DIR>           # Run tests in directory
miri test --filter <PATTERN>    # Run tests matching pattern
miri test --format pretty|json  # Output format
miri test -v                    # Verbose
miri test --verify-mir          # Verify MIR optimization pass
```

## Test File Rules

A test file holds only declarations (types, functions, test functions). It must not declare `fn main` and must not have executable statements outside a function. The `miri test` runner enforces both rules:

**Correct test file:**

```miri
use system.testing.{assert_eq}

@test fn my_test()
    assert_eq(1, 1)

@test fn another()
    assert_eq(2, 2)
```

**Declaring `fn main` is rejected:**

The runner says:
```
declares its own `main`; a test file holds only declarations, and the runner supplies the entry point
```
The runner appends its own `main` dispatcher, so any user-declared `main` causes a conflict.

**Top-level executable statements are rejected:**

The runner says:
```
has executable statements outside a function; move them into a `@test` function, where they would otherwise be silently skipped
```

**Non-parsing test files are rejected:**

The runner says:
```
declares `@test` but does not parse; run `miri check` on it for the syntax error
```
A test file must be valid Miri syntax; files with syntax errors are rejected without running any tests.

**A file that does not compile is rejected too, and its errors are printed once:**

The runner says:
```
does not compile, so none of its tests was built; the errors follow, and `miri check` on the file reports the same ones
```
The errors belong to the file, not to any one test, so none of its tests reports a verdict. With `--format json` they arrive in the envelope's `diagnostics` array carrying the same `help` and `repair` a `check` of that file gives, so a missing `use system.testing` is one `miri fix --apply --yes` away.

**Why these rules have no `fails=` blocks:**

Files with `fn main`, top-level statements, or parse errors type-check cleanly (or fail to parse entirely), so the compiler-driven gate cannot express them as `fails=` blocks. Only the `miri test` runner enforces these rejection rules at runtime.

To run executable setup code outside a test, write a separate `.mi` file with a regular `fn main()` instead.

## Module Resolution in Tests

`miri test --dir <DIR>` works from any working directory. Imports in a test file resolve against the **entry file's directory** (where the test file is located), not the shell's current working directory. Set `MIRI_STDLIB_PATH` to override the stdlib search when the binary is not beside `stdlib/`.

## Anti-Hallucination: Test Syntax That Does Not Exist

### Test Functions Must Take No Parameters

Test functions must have zero parameters:

```miri,fails=MER_TYP_017,expects-message=test functions must take zero parameters
@test fn bad_test(x int)
    println("test")
```

### Test Functions Must Not Have Return Types

Test functions must have no explicit return type:

```miri,fails=MER_TYP_017,expects-message=test functions must not declare a return type
@test fn bad_test() int
    5
```

### Assertions Require an Import

Assertion functions are not in the prelude:

```miri,fails=MER_TYP_034,expects-message=Undefined variable: assert_eq
@test fn test_add()
    assert_eq(2 + 2, 4)
```

Always import from `system.testing`:

```miri
use system.testing.{assert_eq}

@test fn test_add()
    assert_eq(2 + 2, 4)
```

## Test Discovery and Execution

The `miri test` runner scans all `.mi` files in a directory and its subdirectories, finds `@test fn` declarations, and executes each one. Output follows this format:

```
running N tests
test <file>::<function> ... ok
test <file>::<function> ... FAILED

test result: ok. N passed; 0 failed; 0 ignored
```

A test passes if it runs without panicking. Any unhandled panic causes a FAILED result.

## Workflow

1. Write a test function with `@test fn name()` and call assertions.
2. Run `miri test --dir .` to execute all tests.
3. Fix failures by adjusting the code or the assertion logic.
4. Use `--filter <pattern>` to run a subset during development.

miri-gpu

GPU kernels, forall loops, gpu fn, residency, and gpu frame blocks. View on GitHub ↗

Show the full pack
---
name: miri-gpu
description: GPU kernels, forall loops, gpu fn, residency, and gpu frame blocks
---

# GPU Programming in Miri

Miri compiles to GPU through the `forall` construct and `gpu` keyword. This skill teaches how to write GPU kernels, manage device memory, and interact with the GPU execution model.

## GPU Fundamentals

### Implicit Launch with `forall`

The simplest GPU pattern: declare a `gpu var` and iterate over it with `forall`:

```miri
use system.collections.array

const N = 1024

gpu var data = Array<f32, N>()

forall i in 0..N
    data[i] = 2.0
```

Miri infers the grid dimensions from the range and array size.

### Multi-Dimensional Launches

`forall` supports 2-D and 3-D iteration. Literal bounds fold into the launch grid; runtime bounds use uniforms:

**2-D and 3-D launches (top-level only):**

Multi-dimensional `forall` is only valid at the top level, not inside `gpu frame` blocks. Use them to initialize buffers or set up geometry:

```miri
use system.collections.array

const W = 16
const H = 16

gpu var canvas = Array<int, 256>()

forall x, y in 0..W, 0..H
    canvas[y * W + x] = x + y * 100
```

For 3-D:

```miri
use system.collections.array

const W = 8
const H = 8
const D = 4

gpu var volume = Array<f32, 256>()

forall x, y, z in 0..W, 0..H, 0..D
    volume[z * H * W + y * W + x] = (x as f32) + (y as f32) + (z as f32)
```

**Note:** Inside a `gpu frame` block, all `forall` passes must be 1-dimensional (e.g., `forall idx in 0..N`). Derive 2-D or 3-D coordinates from the flat index using modulo and division.

For a simple launch, initialize a buffer at the top level:

```miri
use system.gpu
use system.collections.array

const N = 256
gpu var data = Array<int, N>()

forall i in 0..N
    data[i] = i * 2
```

### Explicit Kernel Functions

For more control, write a `gpu fn` (kernel function):

```miri
use system.gpu
use system.collections.array

gpu fn fill(dst out Array<int, 4>)
    let i = kernel.global_idx.x
    dst[i] = i

fn main()
    gpu var out = Array<int, 4>()
    fill(out).launch(Dim3(4, 1, 1), Dim3(1, 1, 1))
```

The `.launch()` method takes grid and block dimensions. The block size must be
written as literals in the `Dim3(...)` call; a named constant is refused at build
(MER_CG_008).

Atomic buffers are `Array<Atomic<i32>, N>` or `Array<Atomic<u32>, N>` from
`use system.gpu.atomic`, updated in a kernel with `atomic_add`, `atomic_sub`,
`atomic_max`, `atomic_min`, `atomic_and`, `atomic_or`, `atomic_xor`,
`atomic_exchange` and `atomic_compare_exchange`. Each takes the buffer, an index
and a value, and returns the element it found. There is no `atomic_load` or
`atomic_store`: `atomic_add(buf, i, 0 as i32)` reads atomically and
`atomic_exchange(buf, i, v)` writes. On the host, read the buffer back with
`let host = buf`, without a declared type; its elements read as the integers
they hold.

```miri
use system.gpu
use system.gpu.atomic
use system.collections.array

gpu var hist = Array<Atomic<i32>, 4>()
gpu forall i in 0..8
    atomic_add(hist, i % 4, 1 as i32)
let host = hist
println(f"{host[0]}")
```

### Kernel Context

Inside a `gpu fn`, access thread and grid information via the `kernel` object:

```miri
use system.gpu
use system.collections.array

gpu fn copy(src Array<f32, 16>, dst out Array<f32, 16>)
    let thread_idx = kernel.thread_idx.x
    let block_idx = kernel.block_idx.x
    let global_idx = kernel.global_idx.x
    let block_dim = kernel.block_dim.x
    let grid_dim = kernel.grid_dim.x
    let warp_size = kernel.warp.size
    let lane_id = kernel.warp.lane_id
    dst[global_idx] = src[global_idx]
```

Available fields: `thread_idx`, `block_idx`, `global_idx`, `block_dim`, `grid_dim`, and `warp.size` / `warp.lane_id`. Each position is a `Dim3` with `.x`, `.y`, `.z` components.

### Residency: Host vs Device

Use `gpu var` to allocate on the GPU; `var` allocates on the host:

```miri
use system.collections.array

fn main()
    var host_array = Array<f32, 4>()
    gpu var device_array = Array<f32, 4>()
    device_array = host_array  // Cross-residency assignment
```

Assignments between host and device are allowed at scope boundaries. Inside a kernel, all data is device-resident.

### Frame Rendering

Declare a `gpu frame` block to run a per-frame GPU computation:

```miri
use system.gpu
use system.collections.array

const WIDTH = 8
const HEIGHT = 8
gpu var canvas = Array<int, 64>()

gpu frame
    forall idx in 0..(WIDTH * HEIGHT)
        let x = idx % WIDTH
        let y = idx / WIDTH
        let distance = ((x as f32 - frame.mouse_x * WIDTH as f32) * (x as f32 - frame.mouse_x * WIDTH as f32) + 
                        (y as f32 - frame.mouse_y * HEIGHT as f32) * (y as f32 - frame.mouse_y * HEIGHT as f32)) as i32
        canvas[idx] = frame.time as i32 + distance
```

The `gpu frame` block runs every display frame and can access frame input fields. The complete list of 14 available fields:

| Field | Type | Purpose |
|-------|------|---------|
| `time` | f32 | Elapsed time in seconds |
| `dt` | f32 | Delta time since last frame |
| `index` | int | Frame counter (0, 1, 2, ...) |
| `mouse_x` | f32 | Normalized mouse X (0.0–1.0) |
| `mouse_y` | f32 | Normalized mouse Y (0.0–1.0) |
| `mouse_down` | bool | True if mouse button is held |
| `drag_dx` | f32 | Accumulated drag X while button held |
| `drag_dy` | f32 | Accumulated drag Y while button held |
| `wheel` | f32 | Scroll wheel delta this frame |
| `clicked` | bool | True if mouse button clicked this frame |
| `double_clicked` | bool | True if double-click this frame |
| `move_x` | f32 | Pointer movement X this frame (any motion, not just drags) |
| `move_y` | f32 | Pointer movement Y this frame (any motion, not just drags) |
| `hovering` | bool | True if pointer is over the canvas |

## GPU Scalar-Width Restrictions

Not all scalar types are valid in GPU device memory. Miri enforces strict portability rules:

- **`bool` is invalid as a buffer element:** bool cannot be stored in arrays or buffers on GPU. Use `i32` (0 or 1) instead.
- **64-bit scalars (`i64`, `u64`, `f64`) are adapter-feature-gated:** They compile on some adapters but fail at runtime on mobile and WebGPU, making them a portability hazard.
- **Valid on all adapters:** `i32`, `u32`, `i16`, `u16`, `f32`.

Attempting to create a GPU array with `bool` fails at compile time:

```miri,fails=MER_TAR_007,expects-message=does not implement 'Accelerable' and cannot be gpu-resident
use system.collections.array

gpu var buf = Array<bool, 4>()

gpu frame
    forall i in 0..4
        buf[i] = true
```

Use `i32` instead:

```miri
use system.collections.array

gpu var buf = Array<i32, 4>()

gpu frame
    forall i in 0..4
        buf[i] = 1  // 1 represents true
```

## Anti-Hallucination: GPU Syntax That Does Not Exist

### GPU Functions Must Not Have Return Types

GPU kernels return values through `out` parameters only:

```miri,fails=MER_TAR_008,expects-message=GPU functions must not have an explicit return type
use system.gpu

gpu fn twice(x int) int
    x * 2
```

Correct form uses `out` parameters with array buffers:

```miri
use system.gpu
use system.collections.array

gpu fn double(input Array<int, 16>, result out Array<int, 16>)
    let i = kernel.global_idx.x
    result[i] = input[i] * 2
```

## GPU Test Patterns

Test GPU code like host code — it type-checks without a GPU adapter:

```miri
use system.gpu
use system.collections.array

gpu fn init(data out Array<int, 16>)
    let i = kernel.global_idx.x
    data[i] = i * 2

fn main()
    gpu var result = Array<int, 16>()
    init(result).launch(Dim3(16, 1, 1), Dim3(1, 1, 1))
```

Run with `miri check` to verify syntax and types. No simulator or device is required; only the actual `miri run` command with GPU support needs hardware.

Traps & Differences

These are the mistakes that cost agents the most in our trials. They are in the miri-lang pack, and are worth a read before you review Miri code an agent wrote.

Where Miri differs

  • Bindings are let (immutable) and var (mutable); there is no let mut. Parameters are immutable too; mutate a collection argument with .set(i, v).
  • Types follow the name with a space: let x int = 5, fn f(a int) int. No : before a type, no ->, no literal suffixes (42i32); cast with as.
  • Logic is and, or, not. Nullable is T? with bare Some/None; Result patterns need the prefix: Result.Ok(v), Result.Err.
  • List([1, 2]) builds from a literal, List<int>() is empty, Map<K, V>() and Set<T>() likewise. [1, 2, 3] alone is a fixed Array; {} is a Map.
  • A for over a Map walks its keys; there is no .keys().
  • An f-string is the only int → String conversion: f"{n}", never n.to_string() or "n=" + n.
  • main() int returns an exit status; panic(message) stops with status 1. There is no exit.
  • Stdlib modules are imported by name (use system.collections.list).

Traps

  • Quotes inside f-string braces must be the other quote character: f"{m['a']}". The same quote ends the string (MER_LEX_012).
  • Positions count characters, in length(), substring, element_at, code_at and regex matches alike; only size() counts bytes. A substring range outside the text stops the program (or fails to compile, if constant).
  • Integer overflow stops the program (MER_RT_003) instead of wrapping. Where wrapping is meant — a hash, a checksum — write wrapping_add, wrapping_sub or wrapping_mul from system.math.
  • Walk with for, not by position. for ch in s and for k in m are one pass; indexing a String, Map or Set by a counter repeats lookups.
  • Every statement lives in a function. A statement beside main never runs and is refused (MER_TYP_069); put the script’s body inside main.
  • Integer widths do not convert implicitly. An int value stored into an i32/u32 slot, element or field needs as i32; the same holds between float and f32. miri fix --apply writes the cast.
  • Format specs follow Python: f"{x:.2f}", f"{n:>8}", f"{n:08x}". A precision needs f ({x:.2} is refused); x/b/o take integers only.
  • Imports are explicit. A name from another module needs its use line; miri fix main.mi --apply --yes adds one that resolves in exactly one module.

JSON Reference

Every --format json answer follows one schema, diagnostics-schema.json. New fields may appear over time, so ignore any you do not know; schemaVersion changes only when an existing field changes meaning. Fields marked always are present in every answer.

The answer

FieldTypeMeaning
schemaVersion alwaysintegerVersion of this schema. Currently 1. A version gains optional fields and new command values as commands land, so a consumer must ignore members it does not recognise; the number changes only when an existing field changes shape or meaning.
ok alwaysbooleanWhether the command did what was asked. Semantics are per-command; see the per-command table in agent-protocol.md.
command alwaysstringThe command that produced this envelope: ‘check’, ‘build’, ‘run’, ‘test’, ‘explain’, ‘fix’, ‘determinism’, ‘view’, ‘patch’, or ‘skill’.
diagnostics alwaysarray of JsonDiagnosticArray of diagnostic messages (errors, warnings, notes).
artifactstring or nullPath to the build artifact (for build/run commands only). Absent or null if no artifact was produced.
exitCodeinteger or nullProcess exit code. Semantics vary by command: for run, this is the program’s exit status; for other commands, 0 means ok, non-zero means failed. See the per-command table in agent-protocol.md.
signalinteger or nullSignal number if the process was killed by a signal (for run/test commands on Unix only). Absent or null if the process exited normally or on non-Unix platforms.
stdoutTailstring or nullLast N bytes of the program’s stdout (for run/test commands only). Absent or null if not captured.
stderrTailstring or nullLast N bytes of the program’s stderr (for run/test commands only). Absent or null if not captured.
stdoutTruncatedboolean or nullTrue if stdoutTail was truncated; false if complete. Absent or null if stdout was not captured.
stderrTruncatedboolean or nullTrue if stderrTail was truncated; false if complete. Absent or null if stderr was not captured.
durationMsinteger or nullElapsed time in milliseconds for the command execution. Absent or null if not measured.
testsJsonTestSummaryTest summary (for test command only). Absent or null for other commands.
explanationJsonExplanationDocumentation for one diagnostic code (for explain command only). Absent or null for other commands.
viewJsonViewThe canonical source a scoped read returned (view command only). Absent or null for other commands.
patchJsonPatchPatch results: applied edits and revalidation count (patch command only). Absent or null for other commands.
skillsarray or nullSkills from the embedded catalogue (skill command only). Absent or null for other commands.
codesarray or nullDiagnostic code registry (explain list command only). Absent or null for other commands.
gpuJsonGpuTelemetryWhat the run’s GPU work cost (run command only). Absent for a program that never reached the GPU runtime, which is what distinguishes no GPU work from GPU work that cost nothing.

A diagnostic

FieldTypeMeaning
severity alwaysstringSeverity level of the diagnostic.
codestring or nullStable diagnostic code (e.g., ‘MER_TYP_010’) for documentation and tooling. Absent or null for some synthetic diagnostics.
message alwaysstringHuman-readable diagnostic message.
pathstring or nullSource file path where the issue occurred. Absent or null for diagnostics without a source location.
lineinteger or null1-indexed line number in source where the issue occurred. Absent or null if no source location.
columninteger or null1-indexed column number in source where the issue occurred. Absent or null if no source location.
lengthinteger or nullLength in bytes of the error span. Absent or null if no source location.
expectedstring or nullExpected type/value for type mismatch errors. Populated for explicitly-typed error variants (TypeMismatch, ArityMismatch, etc) and selectively for family-coded errors (e.g., MER_TYP_002). Absence does not imply types are unknown; some families do not yet carry structured type information.
actualstring or nullActual type/value for type mismatch errors. Populated for explicitly-typed error variants (TypeMismatch, ArityMismatch, etc) and selectively for family-coded errors (e.g., MER_TYP_002). Absence does not imply types are unknown; some families do not yet carry structured type information.
helpstring or nullActionable help text explaining how to resolve the issue. Absent or null if no help is available.
fixSafetystring or nullFix-safety level of this diagnostic’s repair, if the diagnostic carries a repair. One of: ‘format-only’, ‘behavior-preserving’, ‘local-edit’, ‘api-changing’, ‘target-changing’, or ‘requires-human-review’. It classifies the risk of the edit, not whether one exists — the presence of the ‘repair’ member is what says that. Absent or null if the diagnostic carries no repair.
repairJsonRepairAlways absent in schema version 1; reserved for the repair-plan surface in future versions.
related alwaysarray of JsonRelatedRelated diagnostics (notes, additional context). Empty if no related diagnostics.
preexistingboolean or nullTrue if this diagnostic existed in the pre-edited version (patch command only). False if the edit introduced it. Absent or null for commands other than patch, for a patch command’s own refusal notice (e.g., MER_BLD_011), or for a warning, which is never partitioned against the pre-edited version.

A repair

FieldTypeMeaning
id alwaysstringStable identifier for the repair shape, such as ‘let-to-var’ or ‘add-import’. miri explain --list --format json names the repairs each code can offer.
summary alwaysstringHuman-readable summary of the repair.
edits alwaysarray of JsonEditThe edits that carry out this repair, in source order. Never empty: a repair exists only when the compiler determined the edit.

An edit

FieldTypeMeaning
path alwaysstringFile path (absolute or relative) to edit.
start alwaysintegerStart byte offset (inclusive).
end alwaysintegerEnd byte offset (exclusive).
replacement alwaysstringText replacing the range. Empty to delete the range; the range may be empty to insert.

A test run

FieldTypeMeaning
total alwaysintegerTotal number of tests run (including passed, failed, ignored, etc).
passed alwaysintegerNumber of tests that passed.
failed alwaysintegerNumber of tests that failed.
ignored alwaysintegerNumber of tests that were ignored.
results alwaysarray of JsonTestResultIndividual test results.
rejectedFiles alwaysarray of JsonRejectedFileFiles that were rejected from the test run (e.g., unparseable or declaring main).
filesRead alwaysintegerNumber of .mi files discovery opened and read, whether or not they declared a test. With total 0 it separates ‘read files, none declared a test’ from ‘read no files at all’.

One test

FieldTypeMeaning
path alwaysstringPath to the test file.
name alwaysstringName of the test.
outcome alwaysstringOutcome of the test.
detailstring or nullAdditional detail about the test result (e.g., error message or timing). Absent or null if no detail.
codestring or nullDiagnostic code for assertion failures (e.g., “MER_RT_005”). Absent or null if no failure.
lineinteger or null1-indexed line number where the assertion failed. Absent or null if not applicable.
columninteger or null1-indexed column number where the assertion failed. Absent or null if not applicable.
expectedstring or nullFor assert_eq, the expected value; for assert_panics, the expected message. Absent or null if not applicable.
actualstring or nullFor assert_eq, the actual value; for assert_panics, the caught message. Absent or null if not applicable.
expressionstring or nullThe expression text for bare assert, or null/absent for other assertions.
messagestring or nullOptional user-provided message for the assertion.