HIR MIR LIR (LLVM IR)
Time to time, and especially for learning purposes, it’s useful to inspect the internal representations that the Rust compiler generates. Below are some notes on how to do that.
First, update Rust to ensure you have the latest nightly toolchain:
rustup update
Install log:
info: syncing channel updates for stable-x86_64-pc-windows-msvc
info: syncing channel updates for nightly-x86_64-pc-windows-msvc
info: latest update on 2026-04-26 for version 1.97.0-nightly (9838411cb 2026-04-25)
info: downloading 7 components
rust-src installed 3.85 MiB
cargo installed 9.70 MiB
clippy installed 3.92 MiB
rust-docs installed 22.49 MiB
rust-std installed 21.44 MiB
rustc installed 68.29 MiB
rustfmt installed 2.47 MiB
stable-x86_64-pc-windows-msvc unchanged - rustc 1.95.0 (59807616e 2026-04-14)
nightly-x86_64-pc-windows-msvc updated - rustc 1.97.0-nightly (9838411cb 2026-04-25)
info: checking for self-update (current version: 1.29.0)
info: cleaning up downloads & tmp directories
Now we need a very simple Rust code
fn main() {
/*
Immutable Borrow:
- print_value receives &i32, a reference to x.
- Ownership stays with main.
- x is still usable afterward because it was only borrowed, not moved.
*/
let x = 10;
print_value(&x); // borrow x
println!("x is still usable: {}", x);
/*
Mutable Borrow (allows modification):
- &mut i32 gives temporary permission to modify y.
- Only one mutable borrow is allowed at a time.
- After the borrow ends, y is usable again.
Remember
Rust's primitive integers (i32, u32, etc.) implement the Copy trait.
This means:
let a = 5;
let b = a; // copies, does NOT move
*/
let mut y = 10;
add_one(&mut y); // mutable borrow
println!("y after modification: {}", y);
}
fn print_value(n: &i32) {
println!("The value is {}", n);
}
fn add_one(n: &mut i32) {
*n += 1; // dereference and modify
println!("The new value is {}", n);
}
To inspect the compiler’s High‑Level Intermediate Representation (HIR), run:
cargo +nightly rustc -- -Zunpretty=hir > hir.log
This writes the HIR output into hir.log.
Example HIR Output
HIR is desugared, so we’ll see:
- explicit calls to
std::io::_print - lowered formatting macros
- explicit lifetime annotations (
'_') - desugared blocks and scopes
This is expected — HIR is not meant to look like your original Rust code; it’s the compiler’s cleaned‑up, type‑checked representation:
extern crate std;
#[attr = PreludeImport]
use std::prelude::rust_2024::*;
fn main() {
/*
Immutable Borrow:
- print_value receives &i32, a reference to x.
- Ownership stays with main.
- x is still usable afterward because it was only borrowed, not moved.
*/
let x = 10;
print_value(&x); // borrow x
/*
Mutable Borrow (allows modification):
- &mut i32 gives temporary permission to modify y.
- Only one mutable borrow is allowed at a time.
- After the borrow ends, y is usable again.
Remember
Rust's primitive integers (i32, u32, etc.) implement the Copy trait.
This means:
let a = 5;
let b = a; // copies, does NOT move
*/
// mutable borrow
// dereference and modify
{
::std::io::_print({
super let args = (&x,);
super let args = [format_argument::new_display(args.0)];
unsafe {
format_arguments::new(b"\x13x is still usable: \xc0\x01\n\x00",
&args)
}
});
};
let mut y = 10;
add_one(&mut y);
{
::std::io::_print({
super let args = (&y,);
super let args = [format_argument::new_display(args.0)];
unsafe {
format_arguments::new(b"\x16y after modification: \xc0\x01\n\x00",
&args)
}
});
};
}
fn print_value(n:
&'_ i32) {
{
::std::io::_print({
super let args = (&n,);
super let args = [format_argument::new_display(args.0)];
unsafe {
format_arguments::new(b"\rThe value is \xc0\x01\n\x00",
&args)
}
});
};
}
fn add_one(n:
&'_ mut i32) {
*n += 1;
{
::std::io::_print({
super let args = (&n,);
super let args = [format_argument::new_display(args.0)];
unsafe {
format_arguments::new(b"\x11The new value is \xc0\x01\n\x00",
&args)
}
});
};
}