miri-lang · diff
git:20261011.765476e to git:20261011.30ac483
63 added, 319 removed. Audit A to A.
---
name: miri-lang
description: Writing Miri source files — syntax, type system, and iteration workflow
---
# Miri Language Essentials
- This skill teaches the core syntax and idioms for writing `.mi` source files. It covers what the language supports, what it explicitly does not support, and the workflow for arriving at correct code.
+ 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.
- ## Positive Grammar
+ ## Core Syntax
- Miri's syntax is indentation-sensitive. Variables use space-separated type annotations (not colon form). Here is the core:
+ 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
- fn add(a int, b int) int
- a + b
-
- fn demo_color(color Color)
- match color
- Color.Red: println("red")
- Color.Blue: println("blue")
-
- class Shape
- fn area() int: 0
-
trait Drawable
fn draw()
class Circle implements Drawable
fn draw(): println("circle")
- fn identity<T>(x T) T
- x
+ fn add(a int, b int) int
+ a + b
fn main()
let x int = 5
- var y int = 10
- y = 20
+ var y = 10
+ y = add(x, y)
let p = Point(3, 4)
- let sum = add(x, y)
+ var words = Map<String, int>()
+ words["a"] = 1
let items = List([1, 2, 3])
- println(f"{sum} {p.x} {items.length()} {identity(7)}")
- demo_color(Color.Red)
- match Result.Ok(5)
+ 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? = None
+ let maybe int? = words.get("a")
match maybe
- Some(v): println(f"some {v}")
- None: println("none")
+ Some(v) if v > 0: println(f"some {v}")
+ Some(_) | None: println("none")
```
- This core powers all Miri programs. The rest of this skill covers what the language **does not** support, because those gaps matter just as much as the syntax that works.
-
- ## Anti-Hallucination: Syntax That Does Not Exist
-
- Miri deliberately leaves these off the surface to avoid ambiguity. Every example below is a compile error. Before guessing a member name, ask: `miri view --type String --public` lists what can be called on a type, including what it inherits and what a trait supplies, and needs no path for a prelude type.
-
- ### Type Annotations on Bindings
+ ## Where Miri Differs
- The colon form does not exist. Use space-separated type annotations instead:
+ - 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
```
- Write the type after the name, and cast when it cannot be inferred:
-
- ```miri
- let x int = 5
- let z = 5 as i64
- ```
-
- ### Literal Type Suffixes
-
- No suffix syntax like `1i32` or `3.14f64`. Miri infers numeric width from
- context; cast when you need a particular one.
-
- ```miri,fails=MER_TYP_034,expects-message=Undefined variable: i32
- let x = 42i32
- ```
-
- ### Result and Option Match Arms: The Asymmetry
-
- **Result** requires the constructor prefix (`Result.Ok`, `Result.Err`). Omitting it causes a type error:
-
```miri,fails=MER_TYP_038,expects-message=Expected enum variant pattern like Result.Ok
fn main()
- let r = Result.Ok(5)
- match r
+ match Result.Ok(5)
Ok(x): x
Err: 0
```
- Correct form uses the prefix:
-
- ```miri
- fn main()
- let r = Result.Ok(5)
- match r
- Result.Ok(x): x
- Result.Err: 0
- ```
-
- **Nullability (`T?`)** uses bare `Some` and `None` without a prefix:
-
- ```miri
- fn main()
- let x int? = 5
- match x
- Some(v): v
- None: 0
- ```
-
- The key difference: `Result` is a standalone type that requires its constructor name in patterns; `T?` is syntactic sugar for nullability where `Some`/`None` are bare constructors.
-
- ### Match Arm Bodies: One Line or an Indented Block
-
- An arm header may end with a colon or not, and its body may sit on that line or
- indented below it. All four spellings mean the same thing. An indented body may
- hold several statements; a same-line body is one expression. Alternative
- patterns are separated by `|`, and an arm may carry an `if` guard:
-
- ```miri
- fn weigh(n int) int
- var weight = 0
- match n
- m if m > 10:
- weight = weight + 10
- weight = weight + 5
- 0 | 1: weight = weight + 1
- _
- weight = weight + 2
- weight
-
- fn main()
- println(f"{weigh(50)} {weigh(0)} {weigh(5)}")
- ```
-
- ### Collection Constructor Forms
-
- The three forms are distinct and mean different things:
-
- - `List([1, 2, 3])` — allocate and populate from a literal
- - `List<int>()` — allocate empty with explicit type argument
- - `{}` — an empty `Map<void, void>` (not a Set or List)
-
- Confusing them is the most common error:
-
```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() // Missing generic type argument
- ```
-
- Correct forms:
-
- ```miri
- use system.collections.list
- use system.collections.map
- use system.collections.set
-
- fn main()
- let l = List([1, 2, 3])
- let empty = List<int>()
- let m = Map<String, int>()
- let s = Set<int>()
- ```
-
- ### No `let mut` — Use `var` Instead
-
- The keyword `let mut` does not exist:
-
- ```miri,fails=MER_PAR_001,expects-message=Expected an end of statement, but found identifier
- let mut x = 5
- ```
-
- Use `var`:
-
- ```miri
- var x = 5
- x = 10
- ```
-
- ### Parameters Are Immutable
-
- Function parameters are immutable, even if the argument is mutable. To mutate the caller's variable, the method must exist in the stdlib or be written as a free function accepting a mutable reference pattern (not yet available). The closest common case:
-
- ```miri,fails=MER_TYP_042,expects-message=Cannot assign to element of immutable variable
- use system.collections.list
-
- fn increment(arr [int]):
- arr[0] = arr[0] + 1
-
- fn main():
- var a = List([5, 10])
- increment(a)
- ```
-
- Methods that mutate use `.set()`:
-
- ```miri
- use system.collections.list
-
- fn increment(arr [int]):
- arr.set(0, arr[0] + 1)
-
- fn main():
- var a = List([5, 10])
- increment(a)
- ```
-
- ## Module Resolution
-
- Bare imports are searched in this order: stdlib (via `MIRI_STDLIB_PATH` env var), the entry file's directory, then the current working directory. Local imports (`use local.*`) resolve only against the entry file's directory. The stdlib cannot be shadowed; `system` is reserved. Set `MIRI_STDLIB_PATH` to override the stdlib location when the binary is not beside `stdlib/`; `miri view --stdlib-root` prints the roots actually searched and whether each exists. `miri view` accepts a module name wherever it accepts a path, so `miri view system.io --outline --public` lists what a module offers without knowing where it lives on disk.
-
- ## Iteration and Control Flow
-
- **For loops over collections and ranges:**
-
- ```miri
- use system.collections.list
-
- fn main()
- let xs = List([1, 2, 3])
- for x in xs
- println(f"{x}")
- for n in 0..3
- println(f"n {n}")
- var i = 0
- while i < 3
- i = i + 1
- ```
-
- **Early return and if/else chains:**
-
- ```miri
- fn classify(n int) String
- if n < 0
- return "negative"
- else if n == 0
- return "zero"
- "positive"
-
- fn main()
- println(classify(-1))
- println(classify(0))
- println(classify(5))
- ```
-
- ## String Conversion and Collections
-
- **f-strings are the only int→String conversion path:**
-
- ```miri
- fn main()
- let n = 42
- let s = f"{n}"
- println("count: " + s)
- println(f"n={n} half={n / 2}")
- ```
-
- Attempting other conversion methods fails:
-
- ```miri,fails=MER_TYP_002,expects-message=cannot add String and int
- fn main()
- let n = 42
- println("count: " + n)
- ```
-
- ```miri,fails=MER_TYP_033,expects-message=Type 'int' does not have members
- fn main()
- let n = 42
- println(n.to_string())
- ```
-
- **Collections — length, push, first(), is_empty, contains:**
-
- ```miri
- use system.collections.list
-
- fn main()
- var l = List<int>()
- l.push(10)
- l.push(20)
- println(f"len {l.length()}")
- println(f"empty {l.is_empty()}")
- println(f"has {l.contains(10)}")
- match l.first()
- Some(v): println(f"first {v}")
- None: println("none")
- ```
-
- **Map — index-set, get, iteration:**
-
- ```miri
- use system.collections.map
-
- fn main()
- var m = Map<String, int>()
- m["a"] = 1
- m["b"] = 2
- println(f"size {m.length()}")
- match m.get("a")
- Some(v): println(f"a {v}")
- None: println("missing")
- for k in m
- println(f"key {k}")
- ```
-
- Map iteration happens over keys. A common mistake is calling `.keys()`:
-
- ```miri,fails=MER_TYP_033,expects-message=has no field or method 'keys'
- use system.collections.map
-
- fn main()
- var m = Map<String, int>()
- for k in m.keys()
- println(k)
+ let l = List()
```
- **String methods:**
+ ## Traps
- ```miri
- fn main()
- let s = " Hello, World "
- let t = s.trim()
- println(t.to_lower())
- println(f"{t.length()}")
- let has = t.contains("World")
- println(f"{has}")
- for part in t.split(", ")
- println(part)
- match "42".to_int()
- Some(n): println(f"parsed {n}")
- None: println("not a number")
- ```
+ - **Quotes inside f-string braces** must be the other quote character:
+ `f"{m['a']}"`. The same quote ends the string (MER_LEX_012).
+ - **`length()` counts characters; `substring(start, end)` takes byte offsets.**
+ On non-ASCII text they disagree silently; check with a test that uses one.
+ - **Integer overflow wraps silently** at the declared width (`int` is 64-bit).
+ Guard a sum that can exceed it before you add.
+ - **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.
+ - **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.
- **Exiting non-zero.** Two ways, and they do not mix: declare `main() int` and
- return the status, or call `panic(message)` from a void function to stop with
- that message on stderr and status 1. `panic` is `void`, so it cannot be the tail
- expression of a `main() int`. There is no `exit`:
+ ## Looking things up
- ```miri,fails=MER_TYP_034,expects-message=Undefined variable: exit
- fn main()
- exit(3)
- ```
+ Ask before guessing a member, a module or a signature. These are cheap and exact:
- ```miri
- fn main() int
- let ok = false
- if ok
- return 0
- return 3
- ```
+ - `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.
- ```miri
- fn main()
- let ok = false
- if ok
- println("done")
- else
- panic("nothing to do")
- ```
+ 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`. The envelope gives each diagnostic a `code`, a `help` and, where one exists, a `repair` with its edits. A diagnostic carrying no `repair` is one you have to edit yourself, and that is the whole reason to read the envelope rather than the text. The repair column `miri explain --list` prints is per code, and a code stands for every condition it is raised for, so it is a lower bound: only the `repair` on the diagnostic in front of you says whether this one can be applied.
- 2. **Edit** — where a diagnostic carries a `repair`, `miri fix myfile.mi --apply --yes` writes every recorded repair in one call; there is no separate preview step, because the check already showed the edits. Everything else is an ordinary edit in your own editor. Where the file is already in canonical layout, `miri patch myfile.mi --replace-in-fn name --old text --new text` makes a scoped edit and re-checks the program before anything reaches disk.
+ 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.
-
- Alongside the loop, for API discovery: `miri view --type Name --public` lists what can be called on a type; `miri view myfile.mi --outline --public` gives the surface a caller can reach; `miri view myfile.mi --fn name` reads one function. Without `--raw` the output is canonical, not literal, and a module name works wherever a path does. `miri explain CODE` describes the rule behind a diagnostic code.
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.
- `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.
- `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.
- `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`.