This guide explains how to read Rust’s Mid-level Intermediate Representation (MIR), focusing on the three core concepts you’ll see in a MIR dump: basic blocks, places, and values. By the end, you’ll be able to trace the flow of a function through its MIR, understand what each statement and terminator does, and use this skill to debug optimizations, reason about borrow checking, or simply satisfy your curiosity about what your code compiles to.
What You’ll Need
- A Rust toolchain installed (stable is fine; nightly gives more MIR-related options).
- A small Rust file to practice on (e.g., a function with a few variables and a loop).
- Basic familiarity with Rust syntax (ownership, references, enums).
- No prior MIR knowledge is required.
Step-by-Step
- Generate a MIR dump. The easiest way is to use the nightly compiler and the
-Z dump-mirflag. For example:rustc -Z dump-mir=your_function_name -Z unpretty=mir your_file.rs. If you’re on stable, you can use thecargo rustc -- -Z unpretty=mir(requires nightly) or use the online Rust Playground with the “MIR” button. Alternatively, themir_dumpdirectory will be created with files named likerustc.nodeN.xxxx.mir. - Open the dump and identify the function. Each function gets its own section. The dump starts with something like
fn main() -> ()followed by the MIR body in a text format. You’ll see a list of basic blocks, each with a label likebb0,bb1, etc. - Understand the structure of a basic block. A basic block is a sequence of statements that execute one after another, ending with a terminator that jumps to another block or returns. In the dump, each block looks like:
bb0: { _0 = const 5; _1 = _0; goto -> bb1; }The block label is on the left, then the statements are indented, and the last line is the terminator (here
goto). - Read the statements. Statements are assignments. The left side is a place (where the value goes), and the right side is a value (what is computed). For example,
_0 = const 5;means “store the constant 5 into the place _0”. Places are usually local variables (like_0,_1, …) or projections like(*_1).field. - Understand places. A place is a memory location. Local variables are numbered starting from
_0(which is the return place). A place can be a simple local, or a projection:(*_1).0means “dereference the pointer in _1, then take field 0”. In the dump, you’ll see things like_2 = &(*_1).0which means “take a reference to the field 0 of the value pointed to by _1”. - Read the values. Values on the right side of an assignment can be:
const– a constant (number, string, etc.).move– moves a value from a place (e.g.,move _1).copy– copies a value (for Copy types).&or&mut– creates a reference to a place.- Operators like
+,-,*, etc. (e.g.,_3 = _1 + _2). - Function calls:
_0 = foo(move _1). - Aggregate expressions:
_0 = [const 1, const 2]for arrays or structs.
- Trace the terminators. The terminator determines what happens next. Common terminators:
goto -> bbN– unconditional jump.switchInt(move _1) -> [0: bb2, otherwise: bb3]– a match on an integer (like an if or match).return– returns from the function.drop(_1) -> bbN– drops a value (runs destructor) then jumps.assert– a runtime check that panics if false.call– a function call (but in newer MIR, calls are statements, not terminators).
- Follow the flow. Start at
bb0, execute each statement, then follow the terminator to the next block. Write down the values of places as you go. This is exactly what a debugger would do, but in a simplified form. - Look for the return place. The
_0place is special: it’s the return value. When you see_0 = ..., that’s the value the function will return. The function ends with areturnterminator. - Practice with a real example. Write a function like
fn add(a: i32, b: i32) -> i32 { a + b }. Dump its MIR. You’ll see something like:bb0: { _0 = _1 + _2; return; }This says: add the two arguments (stored in _1 and _2) and put the result in _0, then return. Simple!
Common Mistakes
- Confusing places and values. A place is a location, a value is the data at that location.
_1is a place;copy _1is a value that reads from that place. - Ignoring the difference between move and copy. A move invalidates the source place, a copy does not. In MIR, you’ll see
move _1when ownership is transferred, andcopy _1when the type is Copy. - Misreading projections.
(*_1).0is not the same as_1.0. The former dereferences a pointer first. Always check the parentheses. - Forgetting that _0 is the return place. Many beginners think _0 is the first argument. It’s not. Arguments start at _1.
- Assuming statements execute in order across blocks. Each block is independent; the terminator decides which block runs next. You must follow the edges, not the block numbers.
Tips
- Use the
-Z unpretty=mirflag for a cleaner, more readable output than the raw dump files. - Start with tiny functions. A single arithmetic expression is much easier to trace than a 50-line loop.
- Write down the state. Use a piece of paper or a text file to track the values of places as you step through blocks.
- Compare with the original Rust source. Keep the source open side-by-side with the MIR to see how each construct maps.
- Look for the
StorageLiveandStorageDeadstatements. These indicate when a local variable’s storage is allocated and deallocated, which helps understand lifetimes. - Use the
rustcflag-Z dump-mir=allto see all MIR passes, but that can be overwhelming. Filter by function name if possible.
Troubleshooting
- The dump file is empty or not generated. Make sure you’re using the nightly compiler and the correct function name. The
-Z dump-mirflag requires the function name to match exactly (including the path if it’s in a module). Try-Z dump-mir=allto see everything. - You see a lot of extra blocks with names like
bb1,bb2that don’t seem to correspond to your code. This is normal. The compiler inserts blocks for cleanup (drop glue), panic paths, and other internal logic. Focus on the blocks that contain your actual operations. - The MIR looks different from what you expected. MIR is an intermediate representation; it’s not a direct translation of your source. It includes explicit drops, borrow checks, and other details. Don’t expect a 1:1 mapping.
- You see
Unreachableblocks. These are blocks that the compiler knows can never be reached (e.g., after apanic!). You can ignore them. - The
switchIntterminator is confusing. It’s essentially a match on an integer. The values after the->are the possible values and the target blocks.otherwiseis the default case.
Frequently Asked Questions
What is the difference between MIR and LLVM IR?
MIR is Rust-specific and is used for borrow checking, optimizations, and code generation. LLVM IR is a more generic low-level representation that MIR is lowered to before machine code generation. MIR is easier to read because it preserves Rust types and constructs.
Why do I see _0 as the first local? What is it?
_0 is the return place. It holds the value that the function returns. Arguments start at _1, and other temporaries are numbered sequentially.
How do I see MIR for a specific function in a large project?
Use the -Z dump-mir=function_name flag, but the name must match the fully qualified path. For example, -Z dump-mir=my_crate::my_module::my_function. If that doesn’t work, dump all with -Z dump-mir=all and search the output.
Can I see MIR on stable Rust?
No, the -Z flags are nightly-only. However, you can use the Rust Playground which has a MIR button that shows the MIR for your code without needing a local nightly install.
What does StorageLive(_1) mean?
It’s a statement that tells the compiler that the storage for local variable _1 is now live (i.e., it exists). This is part of the compiler’s memory optimization. You can generally ignore it when reading the flow.
How do I read a drop terminator?
drop(_1) -> bb2 means “run the destructor for the value in place _1, then jump to block bb2”. This is how Rust ensures resources are released at the end of a scope.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.
More cards in this drawer
- 3 OS Updates Meaning: Sony’s 3-Year Promise
- Robot Training Data: 8 Questions to Ask
- Compare Refrigerator Energy Use & Running Costs
- Apple Watch Remote iTunes: Quick Guide
- Best Alternatives to Optical Drives
- Beats Solo 4 50 Hour Battery But No Noise?2.00 %/h