5.9 KiB
Rapport C-repl en C++
Architecture
Lexer (Alaric, lexer.cpp)
Première étape de l'interprétation du langage. Le code à l'état brut est transformé en un tableau de tokens.
Le lexer avance dans le texte en essayant de matcher chaque type de token, parfois à l'aide de regex
, dans un ordre bien choisi.
Lors de cette étape, les constantes littérales sont d'ores et déjà parsées et stockées dans leur type interne.
Parsing (Augustin, parser.cpp)
Le parser transforme le tableau de tokens en un AST (Abstract Syntax Tree)
.
Il procède par backtracking en suivant les règles de la grammaire définie dans types.hpp
.
Analyse statique (Augustin, analysis.cpp)
Avant d'exécuter le programme, cette étape infère le type de chacun des éléments du programme (stocké sous forme d'AST
) et vérifie la validité de chaque opération.
Une "mémoire virtuelle", copie affaiblie de la mémoire actuelle de la repl, sert à stocker les types des variables.
Le type de chaque noeud de l'AST
est calculé récursivement puis la validité des types passés aux opérateurs est vérifiée,
en s'appuyant notamment sur les règles de casting définies dans casting.cpp
.
Évaluation (Alaric, eval.cpp)
Les noeuds de l'AST
sont évalués de manière récursive.
Des exceptions sont utilisées en interne pour traiter les instructions break
, continue
et return
.
Mémoire (Alaric, memory.cpp)
Memory
définit une structure de mémoire réutilisable qui sert lors de l'analyse statique et de l'évaluation.
Elle gère notamment les scopes
.
Types (types.hpp)
Rassemble les définitions de type communes à tout le projet.
Execute (execute.cpp)
Encapsule toutes les étapes de l'interprétation du code brut aux résultats.
Lecture de l'entrée (main.c, input.c)
Gère le traitement des arguments en ligne de commande et l'interface utilisateur de la repl: la partie "interactive" de la repl.
Casting (casting.cpp)
Définit les règles de casting entre les types.
Erreurs (errors.cpp)
Définit les erreurs, leur affichage.
Utilitaires (utils.cpp, debug.cpp)
Fonctions utilitaires et de débogage.
Appropriation
Certaines parties du langage C ne sont pas spécifiées dans le standard et varient ainsi d'un compilateur à l'autre. Nous avons parfois fait des choix arbitraires, en privilégiant l'option la plus naturelle.
La repl ne permet pas les undefined behavior
dans la mesure où ceux-ci sont protégés par le C++.
Par exemple, il n'est pas possible de lire une variable non-initialisée: cela produira une erreur.
Il a été choisi que toute instruction ait une valeur et un type. Cela permet d'afficher une valeur de retour à l'utilisateur lorsqu'il entre une ou plusieurs instructions.
Pour les statements
, ce type est toujours void
, représenté en interne par la valeur monostate
. Cependant, certaines expression-statements
peuvent avoir un type non-void
. Les expressions
ont évidemment une valeur.
Features
Types
Les types int
, double
, void
et les fonctions composées de ces mêmes types sont supportés.
Instructions
Les instructions suivantes sont implémentées:
- Opérateurs arithmétiques binaires (
+
,-
,*
,/
,%
) et unaires (+
,-
) - Opérateurs logiques (
&&
,||
,!
) - Opérateurs de comparaison génériques (
==
,!=
) - Opérateurs de comparaison arithmétique (
<=
,<
,>=
,>
) - Opérateurs d'incrémentation gauche et droite (
++
,--
) - Opérateur virgule (
,
) - Déclarations de variables avec et sans initialisation (
int x;
,int x = 1;
) - Assignation de variables (
x = 1
) - Conditionnelles (
if (..) ..
,if (..) .. else (..) ..
) - Boucles (
for
,while
) avec contrôle (break
,continue
) - Définition de blocs (
{}
) - Déclaration de fonctions avec et sans corps (
int foo(int x);
,int foo(int x) { .. };
) - Appels de fonctions (
foo(1)
)
Scopes et closures
L'interpréteur prend en charge les scopes
de bloc et de fonction, ainsi que pour les for
, comme attendu dans le langage C.
Il gère également les closures
comme il se doit dans un langage impératif non-fonctionnel.
Une implémentation qui convient à ce type langage (mais pas aux langages multi-paradigmes) est une tableau de pointeurs.
Contrôle de flot
La repl ne comporte pas de contrôle de flot avant l'évaluation-même de l'AST. En conséquence, les erreurs associées sont détectées à la volée lors de l'évaluation.
Erreurs
La repl comporte un système d'erreur riche avec messages d'erreur et position dans le code. Celles-ci sont divisées en 4 catégories en fonction de l'étape à laquelle elles interviennent:
SyntaxError
: lexer et parsingControlError
: en raison de l'absence de contrôle de flot préalable, à divers endroitsTypeError
: analyse statiqueRuntimeError
: évaluation
Les RuntimeErrors
sont accompagnées d'une stack trace
contenant la position dans le code des appels de fonction récursifs.
Librairie standard
Les fonctions suivantes sont proposées pour interagir directement avec la repl:
clear_memory()
réinitialise la mémoire (non supporté hors de la scope globale)dump_memory()
affiche l'état actuel de la mémoiredump_history()
affiche l'historique du code passé en entrée
Interface utilisateur
Le système d'entrée est pratique : il permet de se déplacer dans la ligne actuelle et de naviguer dans l'historique du code entré.
Les lignes sont automatiquement numérotées pendant que la saisie.
Pour permettre les sauts de ligne multiples, il est nécessaire de taper ;;
pour indiquer au programme que l'on a terminé son entrée.
Le résultat de l'évaluation du programme est alors affiché.
Tests
Une suite de tests automatisée est disponible : elle teste les différentes parties du code, les fonctionnalités du langage ainsi que chaque type d'erreur.
Des programmes plus conséquents sont disponibles dans ./example
.