Interpréteur µRust 1 : expressions, instructions, variables¶
Vous allez dans ce TP et le prochain écrire un interpréteur pour µRust en suivant les étapes vues en cours.
Découverte du squelette de code¶
Récupérez le squelette de code et exécutez le programme.
Note: sur les machines du département, il faudra peut-être changer votre CARGO_HOME pour pouvoir installer les dépendences.
Exemple
shell$ cargo run
µRust # 1+1*1
1+1*1
shell$
Examinez l'arborescence du projet (par exemple tree .
).
Le répertoire parsing
contient tous les sous-modules du module parsing.
Vous n'avez pas à aller lire les fichiers qui sont dans ce répertoire.
Sinon vous avez 5 autre modules (inutile de lire tous les fichiers en détail pour le moment):
binop.rs
: le type des opérateurs binaires (+, -, *, /, etc). Pour le moment il n'y a que des opérations sur les entiers, vous aurez une version plus compliquée plus tarderror.rs
: le type des erreurs, que ce soit d'analyse syntaxique (parsing) ou d'évaluation. Pas besoin de modifier ce fichier.expression.rs
: le type des expressions arithmétiques, avec les implémentations des traitsDisplay
etParse
. Comme pourbinop.rs
, ce fichier sera amené à s'enrichir.identifier.rs
: le type des identifiants (les variables), grosso modo des chaînes de caractères non mutables et partageables en clonant "sans coût" (cf pointeur intelligentRc
vu dans le cours précédent)parser.rs
: la définition du traitParse
et des erreurs d'analyse syntaxique
Mise en place de la boucle read-eval-print¶
Lancez le programme avec
cargo run
. Lisezmain.rs
et vérifiez que vous comprenez bien ce qu'il fait.Modifiez
main.rs
de sorte à construire l'arbre syntaxique de l'expression saisie par l'utilisateur; pour utiliserExpression::parse(&str)
, il vous faudra importer le module expression (use expression::Expression
) et le trait Parse (use parser::Parse
). Affichez l'expression avec les chaines de format{}
(Display) et{:?}
(Debug). Vous devriez avoir quelque chose comme ceci.
shell$ cargo run
µRust # 1+1*1
(1 + (1 * 1))
BinOp(Const(1), Add, BinOp(Const(1), Mul, Const(1)))
shell$ cargo run
µRust # 1+1*
Cannot parse
shell$
- Créez un fichier
src/eval.rs
et définissez-y une méthodeeval
pour les expressions, en supposant qu'elles sont sans variables, et sans vous préoccuper des erreurs de division par 0.
// eval.rs
//...
impl Expression {
pub fn eval(&self) -> isize {
match self {
/* ... */
Identifier(_) => todo!("plus tard"),
}
}
}
- Appelez la méthode
eval
dans la fonctionmain
demain.rs
pour afficher la valeur de l'expression.
shell$ cargo run
µRust # 1+1*1
- : isize = 2
shell$
- Modifiez la signature de la méthode
eval
, comme vu en cours, pour gérer les erreurs d'évaluation (notamment une division par zéro). Faites afficher les erreurs dans lemain
(profitez de l'implémentation deDisplay
pour les erreurs).
shell$ cargo run
µRust # 1/0
Evaluation Error: Division by zero
shell$
- Ajoutez une boucle dans la fonction
main
pour avoir une vrai boucle REPL (voir cours).
shell$ cargo run
µRust # 1+1*1
- : isize = 2
µRust # 1-1*1
- : isize = 0
^D
shell$
Espaces de noms et instructions let¶
- Ajoutez un fichier
src/namespace.rs
qui déclare unstruct NameSpace
contenant uneHashMap
associant des identifiants à desisize
(voir cours). Implémentez les constructeurs et méthodesnew
,find
etdeclare
. - Ecrivez un module de test dans
src/namespace.rs
pour tester vos méthodes (ou utilisez celui-ci) - Dans
eval.rs
, modifiez la méthodeeval
deExpression
pour prendre en compte l'espace de noms, et enlevez letodo!()
du casIdentifier
. À la suite, écrivez un module de test de votre méthodeeval
qui vérifie que1+x
s'évalue à2
dans un espace de nom oùx
vaut1
, et lève l'erreurUndefined
dans un espace de nom vide. - Lisez la définition du type
Instruction
dans le fichier src/instruction.rs et ajoutez ce fichier à votre projet. - Dans
eval.rs
, définissez une méthodeexec(&self, ns: &mut NameSpace)
qui exécute une instruction. - Reprennez la boucle REPL de la fonction
main
de sorte à non plus évaluer des expressions mais exécuter des instructions
shell$ cargo run
µRust # let x = 0
x : isize = 0
µRust # let y = x + 1
y : isize = 1
µRust # x + y
- : isize = 1
^D
shell$
Piles d'espaces de noms et blocs¶
- On veut ajouter comme suit une ligne au fichier
instruction.rs
:
pub enum Instruction {
Expr(Expression),
Let{id:Identifier, expr:Expression},
Block(Vec<Instruction>), // <- NOUVEAU!
}
Récupérez le fichier instruction.rs (v2) qui fait essentiellement ce changement (mais aussi met à jour le parser et l'affichage d'instructions).
Insérez un
todo!()
dans le match de la méthodeexec
(fichiereval.rs
) pour gérer ce nouveau constructeurBlock
. Vérifiez que votre projet compile à nouveau.Ajoutez un fichier
namespacestack.rs
à votre projet qui implémente une pile d'espaces de noms, comme vu en cours. Implémentez les méthodesnew
,push
,pop
,declare
, etfind
vues en cours.Écrivez un test qui crée une pile avec un espace de noms vide, déclare
x
valant0
, puis déclarey
valant0
, puis empile un nouvel espace de noms vide, et déclarex
valant1
. Vérifiez quefind
renvoie bien1
pourx
,0
poury
, et une erreur pourz
.Dans le fichier
eval.rs
, modifiez l'argument de la méthodeexec
(ce n'est plus unNameSpace
, mais unNameSpaceStack
) puis supprimez letodo!()
de la méthodeexec
et ajoutez du code pour exécuter un bloc d'instructions. En attendant mieux, renvoyez0
pour un bloc d'instruction vide. Modifiez aussi l'argument de la méthodeeval
(idem, ce n'est plus unNameSpace
mais unNameSpaceStack
).Modifiez la fonction
main
de sorte à passer les exemples vus en cours avec des blocs imbriqués.
Sprint final¶
Récupérez les fichiers src/binop.rs (version 2), src/expression.rs (version 2) et src/instruction.rs (version 3) et étendez votre interpréteur de façon à
- gérer des valeurs de types entiers, booléens, et unit
- gérer des expressions conditionnelles et des instructions conditionnelles
- gérer des variables mutables
- gérer des boucles while
Il vous faudra créer un enum Type
pour représenter le type d'une valeur, un enum Value
pour représenter une valeur, décommenter le constructeur TypeMismatch
dans error.rs
, et remplacer la plupart des isize
de votre code par des Value
. Les variables mutables auront un type non mutable: les valeurs qu'elles peuvent prendre doivent rester du même type que leur valeur initiale. Attention à bien évaluer de façon paresseuse les opérateurs booléens comme vu en cours.
Tests: exemples de saisies et sorties attendues
Dépannage¶
Pour changer le CARGO_HOME
sur les machines du département
- éditez la ligne
export CARGO_HOME=
de votre fichier~/.zshrc
(ou~/.profile
, ou~/.zprofile
, ...) comme suit
export CARGO_HOME=~/.cargo_home
- créez le répertoire s'il n'existe pas déjà (
mkdir -p ~/.cargo_home
) - lancez
cargo run
depuis un nouveau terminal