Introduzione a MIR

Nell'ambito della programmazione in Rust, uno degli elementi chiave per comprendere le capacità del linguaggio in termini di sicurezza e prestazioni è la sua rappresentazione intermedia a medio livello, conosciuta come MIR, ovvero Mid-level Intermediate Representation. MIR è una rappresentazione intermedia del codice Rust che si trova tra l'AST (Abstract Syntax Tree) generato dal parser e il codice macchina finale prodotto dal compilatore. Questa rappresentazione gioca un ruolo cruciale in vari aspetti del processo di compilazione, in particolare nell'ottimizzazione e nell'analisi statica del codice.

Il Ruolo di MIR

Nel processo di compilazione di Rust, MIR assume un ruolo fondamentale per diversi motivi. Per prima cosa, a questo livello è possibile effettuare Ottimizzazione del Codice. Infatti, una volta che il codice Rust è stato trasformato in MIR, il compilatore può applicare una serie di ottimizzazioni più aggressive e mirate, perché MIR è una rappresentazione più semplificata e normalizzata del codice, che elimina molte delle complessità e delle astrazioni presenti nel codice sorgente originale.

Uno degli aspetti più importanti di MIR è il fatto che a questo livello viene eseguito il Borrow Checker. MIR è infatti basato su un modello a grafo di control-flow, fondamentale per il funzionamento del borrow checker, permettendo al compilatore di verificare in modo più efficiente e accurato che il codice rispetti le regole di borrowing e lifetime. Dopo l'ottimizzazione e l'analisi, MIR viene ulteriormente trasformato in una rappresentazione ancora più bassa chiamata LLVM IR (Intermediate Representation), che viene poi utilizzata per generare il codice macchina finale da LLVM.

A livello di MIR operano anche moltissimi strumenti che eseguono analisi statica dei programmi, in quanto sono rimosse molte astrazioni ad alto livello, ma comunque nasconde molti dettagli dell'architettura e dell'hardware. Si tratta quindi di un buon compromesso tra basso livello e alto livello. Analizzatori e strumenti possono esaminare il MIR per individuare bug, potenziali problemi di sicurezza e opportunità di miglioramento delle prestazioni senza dover interpretare direttamente il codice sorgente Rust, che può essere più complesso e variegato.

Esempio di MIR

Prendiamo questo codice Rust

fn main() {
    let mut vec = Vec::new();
    vec.push(1);
}

Possiamo visualizzare il codice MIR generato nel playground ufficiale di Rust. Per il codice sopra dovremmo ottenere un output di questo tipo

// WARNING: This output format is intended for human consumers only
// and is subject to change without notice. Knock yourself out.
fn main() -> () {
    let mut _0: ();
    let mut _1: std::vec::Vec<i32>;
    let _2: ();
    let mut _3: &mut std::vec::Vec<i32>;
    scope 1 {
        debug vec => _1;
    }

    bb0: {
        _1 = Vec::<i32>::new() -> [return: bb1, unwind continue];
    }

    bb1: {
        _3 = &mut _1;
        _2 = Vec::<i32>::push(move _3, const 1_i32) -> [return: bb2, unwind: bb4];
    }

    bb2: {
        drop(_1) -> [return: bb3, unwind continue];
    }

    bb3: {
        return;
    }

    bb4 (cleanup): {
        drop(_1) -> [return: bb5, unwind terminate(cleanup)];
    }

    bb5 (cleanup): {
        resume;
    }
}

L'output è costituito da molti basic blocks, e le variabili e i rispettivi drop sono esplicitamente indicati. Inoltre ogni variabile è esplicitamente accompagnata dal suo tipo. Il controllo del flusso è indicato come relazioni tra i basic-blocks. Non è importante capire nel dettaglio questa rappresentazione, ma solo il fatto che stiamo operando a un livello di astrazione intermedio, né troppo basso né troppo alto.