HIR (High-level IR)
HIR (High-level IR)
Level 19 — Rust A desugared, compiler-internal version of the AST used during name resolution and type checking before lowering to MIR.
1. Prerequisites
- Tokens — Lexical tokens.
2. Term Category
Rust Compiler Intermediate Representation (desugared AST high-level IR): High-Level Intermediate Representation (HIR) in rustc.
3. Explanation
(1) Design Motivation — "Why did we design this?"
The Abstract Syntax Tree (AST) generated by the parser contains surface-level syntax details (like for loops, if let matching, and macro invocations).
HIR (High-Level Intermediate Representation) is the desugared, compiler-friendly representation of Rust source code where macros are fully expanded, loops are desugared into basic loop primitives, and full type inference, type checking, and trait resolution occur.
(2) Reality Metaphor
A clean architectural blueprint annotated with structural load weights, plumbing routes, and electrical wiring diagrams desugared from raw hand-drawn sketches.
(3) Rust Code Examples
Short Snippet
// Inspected via: rustc -Zunpretty=hir-tree main.rs
Fuller Example
pub fn desugar_demo(opt: Option<i32>) -> i32 {
// AST syntax:
// if let Some(x) = opt { x } else { 0 }
// Desugared HIR equivalent:
match opt {
Some(x) => x,
None => 0,
}
}
4. Common Mistakes & Pitfalls
Mistake 1: Assuming Macro Expansion Happens on HIR
The mistake: Expecting procedural macros to inspect HIR structures.
Why it is wrong: Macros operate on AST token streams before HIR lowering occurs.
Incorrect:
proc_macro operating on HIR
Fix:
Macros operate on AST TokenStream; HIR is created after macro expansion!
Mistake 2: Confusing AST Node IDs with HIR DefIds
The mistake: Using AST Node IDs across compiler passes.
Why it is wrong: HIR introduces DefId and HirId for stable cross-compilation queries.
Incorrect:
NodeId used for global queries
Fix:
Use HirId and DefId for compiler query lookup!
Mistake 3: Attempting Borrow Checking on HIR
The mistake: Expecting borrow checking lifetime validation to run directly on HIR.
Why it is wrong: Borrow checking requires Control Flow Graphs (CFG) provided by MIR, not HIR.
Incorrect:
Borrow checking HIR directly
Fix:
HIR is lowered to MIR before borrow checking and lifetime validation!
5. Practice Exercises
Exercise 1: AST to HIR Loop Desugaring Simulator
Scenario: Build a parser helper simulating HIR loop desugaring (for x in iter into loop { match iter.next() }).
Requirements:
- Implement
desugar_for_loop(var: &str, iter_expr: &str) -> String. - Return desugared Rust code string.
Answer
Implementation
pub fn desugar_for_loop(var: &str, iter_expr: &str) -> String {
format!(
"let mut iter = {iter_expr}.into_iter(); loop {{ match iter.next() {{ Some({var}) => {{}}, None => break, }} }}"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_loop_desugaring() {
let hir_code = desugar_for_loop("item", "vec");
assert!(hir_code.contains("into_iter()"));
assert!(hir_code.contains("match iter.next()"));
}
}
Technical Explanation
- Demonstrates how HIR lowers surface syntax like
forloops into fundamentalloopandmatchprimitives. - Simplifies downstream type checking.
Exercise 2: HIR Type Inference Map Representation
Scenario: Simulate a type inference map linking HIR Node IDs to resolved types.
Requirements:
- Create
HirTypeTablemapping node IDs to&strtypes. - Insert and query resolved types.
Answer
Implementation
use std::collections::HashMap;
pub struct HirTypeTable {
types: HashMap<u32, &'static str>,
}
impl HirTypeTable {
pub fn new() -> Self {
Self { types: HashMap::new() }
}
pub fn insert_type(&mut self, hir_id: u32, ty: &'static str) {
self.types.insert(hir_id, ty);
}
pub fn get_type(&self, hir_id: u32) -> Option<&&'static str> {
self.types.get(&hir_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hir_type_table() {
let mut table = HirTypeTable::new();
table.insert_type(101, "i32");
assert_eq!(table.get_type(101), Some(&"i32"));
}
}
Technical Explanation
- Represents
rustctype tables linking HIR nodes to inferred types. - Enables static type checking.
Exercise 3: HIR DefId Path Resolver
Scenario: Simulate canonical module path resolution for compiler DefIds.
Requirements:
- Map
DefId(u32)to module paths. - Resolve paths.
Answer
Implementation
use std::collections::HashMap;
pub struct DefMap {
defs: HashMap<u32, String>,
}
impl DefMap {
pub fn new() -> Self { Self { defs: HashMap::new() } }
pub fn register(&mut self, id: u32, path: &str) { self.defs.insert(id, path.to_string()); }
pub fn resolve(&self, id: u32) -> Option<&str> { self.defs.get(&id).map(|s| s.as_str()) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_def_resolution() {
let mut map = DefMap::new();
map.register(1, "std::vec::Vec");
assert_eq!(map.resolve(1), Some("std::vec::Vec"));
}
}
Technical Explanation
- Illustrates compiler
DefIdresolution across module boundaries. - Used extensively in rustc compiler query engine.
6. Related Terms
- MIR (Mid-level IR) — Mid-level IR.
7. Key Takeaways
- HIR is the desugared, compiler-friendly AST representation.
- Performs type inference, static type checking, and trait resolution.
- Desugars
forloops,if let, andwhile letinto fundamentalloop/matchprimitives. - Lowered to MIR for borrow checking and code generation.