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:
| Tool | What it does for the agent |
|---|---|
| The loop | One short routine — check, fix, test — that the agent repeats until the code is clean. |
miri check --format json | Every error and warning as JSON: a stable code, the exact place, a one-line help, and often a ready-made repair. |
miri fix | Applies those repairs to the file in one call. |
miri explain | Describes the rule behind any error code, with a before-and-after example. |
miri view | Answers “what can I call on this type?” and “what does this module offer?” from the real standard library. |
miri test | Runs @test functions and reports each failure with the expected and actual values. |
miri patch | Edits inside one function and refuses the edit if the file would stop compiling. |
| Skill packs | Short 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 agent | Command | Writes to |
|---|---|---|
| Claude Code | miri skill install | .claude/skills/ |
| Cursor, Codex, OpenCode, Windsurf, Gemini CLI | miri skill install --agent agents | .agents/skills/ |
| Anything else | miri skill install --agent generic | skills/ |
$ 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.
- miri check —
miri check myfile.mi --format json. Each diagnostic has acode, ahelpand, where one exists, arepairwith its edits. A diagnostic without arepairis one you edit yourself. - Edit —
miri fix myfile.mi --apply --yeswrites 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 textmakes a scoped edit and re-checks before anything reaches disk. - 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.
// 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 itfalse.exitCode— the same number the process exits with.diagnostics— one entry per problem, each with:code— a stable name such asMER_TYP_042. Pass it tomiri explainfor the full rule.line,columnandlength— 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 offsetsstartandendin the file, and thereplacementtext.miri fixapplies 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:
| Level | Meaning | Applied by default |
|---|---|---|
format-only | Changes layout only. | yes |
behavior-preserving | Changes the text, not what the program does. | yes |
local-edit | Changes one spot, and nothing outside it can notice. | yes |
api-changing | Changes something other code relies on, such as a parameter name callers can pass by name. | no |
target-changing | Changes where or how the program runs, such as moving data to the GPU or narrowing a number. | no |
requires-human-review | Plausible, 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:
| Repair | What it does | Offered with |
|---|---|---|
add-import | Import a name that resolves in exactly one module. | MER_TYP_034 MER_TYP_043 |
arrow-return-type | Drop the -> before a return type. | MER_PAR_001 |
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. | MER_TYP_002 |
colon-annotation | Drop the : before a type annotation. | MER_PAR_001 |
concat-to-formatted-string | Rewrite a + chain joining text to values as one f-string. | MER_TYP_002 |
drop-extra-arguments | Drop positional arguments a call does not declare. | MER_TYP_030 |
drop-iterator-accessor | Drop a keys accessor on a keyed collection and iterate it directly. | MER_TYP_033 |
if-colon | Write : in place of then, or after an inline else. | MER_PAR_001 |
interpolation-quotes | Write the string literals inside an f-string’s braces with the other quote. | MER_LEX_012 |
let-mut-to-var | Rewrite a let mut binding as var. | MER_PAR_001 |
let-to-var | Rebind an immutable declaration as mutable. | MER_TYP_042 |
move-into-main | Move statements written beside main into it, in order, ahead of what it already does. | MER_TYP_069 |
null-to-none | Rewrite null, nil or nullptr as None. | MER_TYP_034 |
println-bang | Drop the ! from a macro-style call. | MER_LEX_001 |
qualify-variant-pattern | Prefix a bare variant pattern with the enum that declares it. | MER_TYP_038 |
remove-unused-import | Delete a use line nothing reads, or the unread names in a selection. | MER_IMP_005 |
rename-to-suggestion | Rewrite 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-binding | Prefix a binding nothing reads with _ to say the value is not meant to be read. | MER_TYP_071 MER_TYP_072 |
word-operator | Rewrite !, && 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:
// 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) andvar(mutable); there is nolet 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 withas. - Logic is
and,or,not. Nullable isT?with bareSome/None;Resultpatterns need the prefix:Result.Ok(v),Result.Err. List([1, 2])builds from a literal,List<int>()is empty,Map<K, V>()andSet<T>()likewise.[1, 2, 3]alone is a fixedArray;{}is aMap.- A
forover aMapwalks its keys; there is no.keys(). - An f-string is the only int → String conversion:
f"{n}", nevern.to_string()or"n=" + n. main() intreturns an exit status;panic(message)stops with status 1. There is noexit.- 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_atand regex matches alike; onlysize()counts bytes. Asubstringrange 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 — writewrapping_add,wrapping_suborwrapping_mulfromsystem.math. - Walk with
for, not by position.for ch in sandfor k in mare one pass; indexing aString,MaporSetby a counter repeats lookups. - Every statement lives in a function. A statement beside
mainnever runs and is refused (MER_TYP_069); put the script’s body insidemain. - Integer widths do not convert implicitly. An
intvalue stored into ani32/u32slot, element or field needsas i32; the same holds betweenfloatandf32.miri fix --applywrites the cast. - Format specs follow Python:
f"{x:.2f}",f"{n:>8}",f"{n:08x}". A precision needsf({x:.2}is refused);x/b/otake integers only. - Imports are explicit. A name from another module needs its
useline;miri fix main.mi --apply --yesadds 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
| Field | Type | Meaning |
|---|---|---|
schemaVersion always | integer | Version 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 always | boolean | Whether the command did what was asked. Semantics are per-command; see the per-command table in agent-protocol.md. |
command always | string | The command that produced this envelope: ‘check’, ‘build’, ‘run’, ‘test’, ‘explain’, ‘fix’, ‘determinism’, ‘view’, ‘patch’, or ‘skill’. |
diagnostics always | array of JsonDiagnostic | Array of diagnostic messages (errors, warnings, notes). |
artifact | string or null | Path to the build artifact (for build/run commands only). Absent or null if no artifact was produced. |
exitCode | integer or null | Process 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. |
signal | integer or null | Signal 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. |
stdoutTail | string or null | Last N bytes of the program’s stdout (for run/test commands only). Absent or null if not captured. |
stderrTail | string or null | Last N bytes of the program’s stderr (for run/test commands only). Absent or null if not captured. |
stdoutTruncated | boolean or null | True if stdoutTail was truncated; false if complete. Absent or null if stdout was not captured. |
stderrTruncated | boolean or null | True if stderrTail was truncated; false if complete. Absent or null if stderr was not captured. |
durationMs | integer or null | Elapsed time in milliseconds for the command execution. Absent or null if not measured. |
tests | JsonTestSummary | Test summary (for test command only). Absent or null for other commands. |
explanation | JsonExplanation | Documentation for one diagnostic code (for explain command only). Absent or null for other commands. |
view | JsonView | The canonical source a scoped read returned (view command only). Absent or null for other commands. |
patch | JsonPatch | Patch results: applied edits and revalidation count (patch command only). Absent or null for other commands. |
skills | array or null | Skills from the embedded catalogue (skill command only). Absent or null for other commands. |
codes | array or null | Diagnostic code registry (explain list command only). Absent or null for other commands. |
gpu | JsonGpuTelemetry | What 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
| Field | Type | Meaning |
|---|---|---|
severity always | string | Severity level of the diagnostic. |
code | string or null | Stable diagnostic code (e.g., ‘MER_TYP_010’) for documentation and tooling. Absent or null for some synthetic diagnostics. |
message always | string | Human-readable diagnostic message. |
path | string or null | Source file path where the issue occurred. Absent or null for diagnostics without a source location. |
line | integer or null | 1-indexed line number in source where the issue occurred. Absent or null if no source location. |
column | integer or null | 1-indexed column number in source where the issue occurred. Absent or null if no source location. |
length | integer or null | Length in bytes of the error span. Absent or null if no source location. |
expected | string or null | Expected 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. |
actual | string or null | Actual 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. |
help | string or null | Actionable help text explaining how to resolve the issue. Absent or null if no help is available. |
fixSafety | string or null | Fix-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. |
repair | JsonRepair | Always absent in schema version 1; reserved for the repair-plan surface in future versions. |
related always | array of JsonRelated | Related diagnostics (notes, additional context). Empty if no related diagnostics. |
preexisting | boolean or null | True 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
| Field | Type | Meaning |
|---|---|---|
id always | string | Stable 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 always | string | Human-readable summary of the repair. |
edits always | array of JsonEdit | The edits that carry out this repair, in source order. Never empty: a repair exists only when the compiler determined the edit. |
An edit
| Field | Type | Meaning |
|---|---|---|
path always | string | File path (absolute or relative) to edit. |
start always | integer | Start byte offset (inclusive). |
end always | integer | End byte offset (exclusive). |
replacement always | string | Text replacing the range. Empty to delete the range; the range may be empty to insert. |
A test run
| Field | Type | Meaning |
|---|---|---|
total always | integer | Total number of tests run (including passed, failed, ignored, etc). |
passed always | integer | Number of tests that passed. |
failed always | integer | Number of tests that failed. |
ignored always | integer | Number of tests that were ignored. |
results always | array of JsonTestResult | Individual test results. |
rejectedFiles always | array of JsonRejectedFile | Files that were rejected from the test run (e.g., unparseable or declaring main). |
filesRead always | integer | Number 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
| Field | Type | Meaning |
|---|---|---|
path always | string | Path to the test file. |
name always | string | Name of the test. |
outcome always | string | Outcome of the test. |
detail | string or null | Additional detail about the test result (e.g., error message or timing). Absent or null if no detail. |
code | string or null | Diagnostic code for assertion failures (e.g., “MER_RT_005”). Absent or null if no failure. |
line | integer or null | 1-indexed line number where the assertion failed. Absent or null if not applicable. |
column | integer or null | 1-indexed column number where the assertion failed. Absent or null if not applicable. |
expected | string or null | For assert_eq, the expected value; for assert_panics, the expected message. Absent or null if not applicable. |
actual | string or null | For assert_eq, the actual value; for assert_panics, the caught message. Absent or null if not applicable. |
expression | string or null | The expression text for bare assert, or null/absent for other assertions. |
message | string or null | Optional user-provided message for the assertion. |