Interpréteur µRust 2 : gestion mémoire

Démarrage

Récupérez le squelette de code et exécutez le programme.

Exemple

shell$ cargo run µRust # 1+1 - : isize = 2 µRust # let x = Ptr::new() [CRASH] shell$

Ce squelette correspond à un corrigé très incomplet du premier TP. Il est recommandé de travailler directement avec ce squelette, et à la fin de fusionner le travail de chacun des TP en un seul projet.

Espaces de noms et instructions let

  1. Ajoutez un fichier memorycell.rs et définissez pour commencer une version très simple de cellule mémoire contenant une valeur
struct MemoryCell {
    mutable: bool,
    value: Value,
}

Notes:

  • laissez les champs privés et faites des méthodes is_mutable, get_value, set_value, le type MemoryCell va encore bien évoluer après
  • get_value et set_value peuvent lever des erreurs (leur type de retour est un Result<??,EvaluationError>). Pour get_value, il n'y a aucune erreur pour le moment, mais cela viendra après.
  • vous utiliserez None comme argument pour construire les erreurs NotMutable, NonAllocatedCell et NonInitializedValue (l'expression n'étant pas connue pour le moment)
  1. Modifiez le fichier src/namespace.rs pour que la HashMap associe des identifiants à des MemoryCell.
pub struct NameSpace(HashMap<Identifier, MemoryCell>);
  1. Vous ne devez pas changer la signature des méthodes existantes (find, set, declare) mais vous devez modifier leur code pour que tout compile.
  2. Dans eval.rs, complétez le todo du cas Instruction:Let de la méthode exec.
  3. Toujours dans eval.rs, complétez le todo du cas Expression:Identifier de la méthode eval.
  4. Compilez et testez
shell$ cargo run µRust # let x = 0 x : isize = 0 µRust # x + 1 - : isize = 1 ^D shell$

Adresses et &

  1. Dans un fichier memory.rs, définissez le type des adresses du tas et de la pile comme vu en cours.
enum Address {
    StackAddress(usize, Identifier),
    HeapAddress(usize)
}

Implémentez le trait Display (ex: @2, @[3,x]).

  1. Dans namespacestack.rs, ajoutez la méthode get_address comme vu en cours.

  2. Dans eval.rs, ajoutez la méthode eval_to_address pour les expressions (voir cours). Votre méthode lèvera une erreur si l'expression n'est pas un identifiant, et sinon appelera get_address pour renvoyer son résultat. Vous pouvez laisser un todo pour le cas où l'expression est un Deref ou un NewPtr (vous traiterez ces cas après).

  3. Dans value.rs, ajoutez le cas où la valeur est un pointeur, et modifiez les méthodes pour tenir compte du changement. Vous devrez aussi modifier type.rs.

pub enum Value {
    Integer(isize),
    Boolean(bool),
    Unit,
    Pointer(Address),
}
  1. Dans eval.rs, complétez le todo du cas AmpersAnd de la méthode eval. Testez.
shell$ cargo run µRust # let x = 0 x : isize = 0 µRust # &x - : Ptr = @[0, x] µRust # {let x = -8; &x} - : Ptr = @[1, x] µRust # {let y = -8; &x} - : Ptr = @[0, x] ^D shell$

Tas

  1. Mettez à jour memorycell.rs pour prendre en compte la possibilité qu'une cellule ne soit pas allouée ou pas initialisée, comme vu en cours.
enum MemoryCell {
    NotAllocated,
    AllocatedCell(AllocatedCell),
}

struct AllocatedCell {
    mutable: bool,
    value: Option<Value,
}

Notes:

  • une cellule non allouée n'est pas mutable
  • une cellule non initialisée est mutable
  1. Ajoutez un fichier heap.rs et déclarez y le type Heap ainsi qu'un constructeur new et la méthode malloc vue en cours.
struct Heap(Vec<MemoryCell>) 
impl Heap { /*... */ }
  1. Dans memory.rs, déclarez le type
struct Memory {
    stack: NameSpaceStack,
    heap: Heap
}

Attention, gros refactoring! Dans eval.rs, modifiez la signature des méthodes eval_to_address, eval, et exec pour prendre en argument un emprunt mutable sur la mémoire (et non juste sur la pile). Par exemple,

pub fn eval(&self, mem: &mut Memory) -> Result<Value, EvalError> { /* ... */ }

Il vous faut aussi mettre à jour main.rs. Vérifiez que tout compile.

  1. Dans eval.rs, traitez le todo du cas NewPtr. Testez.
shell$ cargo run µRust # Ptr::new() - : Ptr = @0 µRust # let x = Ptr::new() x : Ptr = @1
  1. Dans eval.rs, traitez le cas du Free. Testez.
shell$ cargo run µRust # let x = Ptr::new() x : Ptr = @0 µRust # let y = Ptr::new() y : Ptr = @1 µRust # free(x) - : unit = () µRust # let z = Ptr::new() z : Ptr = @0

Deref et WriteAt

Dans eval.rs, modifiez les todo de Deref et WriteAt. Testez.

shell$ cargo run µRust # let ptr = Ptr::new() - : Ptr = @0 µRust # *ptr = 42 - : unit = () µRust # let x = *ptr x : isize = 42 µRust # x - : isize = 42 µRust # *x Evaluation Error: Type mismatch in expression `*x`. Expected: Ptr. Found: isize µRust # x = 43 Evaluation Error: Cell at `x` is not mutable. µRust # free(ptr) - : unit = () µRust # *ptr Evaluation Error: Cell at `*ptr` is not allocated. µRust # let ptr2 = Ptr::new() ptr2 : Ptr = @0 µRust # *ptr2 Evaluation Error: Value in `*ptr2` is not initialized.

Horodatage et détection des use after free

Implémentez la détection de use after free par horodatage de cellule et de pointeurs, comme vu en cours.

Testez à l'aide des exemples vus en cours.

Détection des fuites mémoires

Implémentez la détection de fuites mémoires (et le GC) basé sur le marquage des cellules accessibles (voir cours). Vous appelerez les deux méthodes mark_reachable_cells et unmark_and_sweep après chaque évaluation d'instruction dans main.rs.

Possession, sémantique MOVE, RAII, et GC

Implémentez la sémantique MOVE, le RAII, et le "GC" basé sur la possession, voir cours.