15-rustTermsLevel_19HIR (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


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:

  1. Implement desugar_for_loop(var: &str, iter_expr: &str) -> String.
  2. 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

  1. Demonstrates how HIR lowers surface syntax like for loops into fundamental loop and match primitives.
  2. 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:

  1. Create HirTypeTable mapping node IDs to &str types.
  2. 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

  1. Represents rustc type tables linking HIR nodes to inferred types.
  2. Enables static type checking.

Exercise 3: HIR DefId Path Resolver

Scenario: Simulate canonical module path resolution for compiler DefIds.

Requirements:

  1. Map DefId(u32) to module paths.
  2. 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

  1. Illustrates compiler DefId resolution across module boundaries.
  2. Used extensively in rustc compiler query engine.


7. Key Takeaways

  • HIR is the desugared, compiler-friendly AST representation.
  • Performs type inference, static type checking, and trait resolution.
  • Desugars for loops, if let, and while let into fundamental loop/match primitives.
  • Lowered to MIR for borrow checking and code generation.
Built with LogoFlowershow