Description de l'étape
Vous devez écrire les trois éléments suivants.
- Vous devez définir un type de données (AST) qui corresponde à la syntaxe abstrait de la spécification de Vier.
- Vous devez compléter votre analyseur syntaxique (parser) pour générer une instance de votre AST représentant uniquement le programme analysé. Dans les cas où il n'existe pas de correspondance évidente entre la syntaxe concrète et la syntaxe abstraite, la spécification décrit la nature de la correspondance (sucre syntaxique).
- Vous devez aussi disposer d'un moyen pour écrire (dans un fichier ou sur la console) un programme Vier à partir d'une instance de votre AST. Le programme écrit doit être un programme Vier valide, sans sucre syntaxique et avec le même signification sémantique que l'original. Vous devez aussi faire un effort raisonnable pour indenter le programme écrit.
Classe de l'arbre
Dans le cas le plus simple, vous définirez le type de l'AST par une série de classes case
qui héritent d'une classe Tree
commune. Vous pouvez aussi imbriquer toutes les classe de l'AST dans un objet qu'il sera ensuite facile d'importer. L'objet Trees
est un tel objet et dispose de l'interface suivante.
object Trees { trait Tree { def pos: Position def setPos(pos: Position): tree.type } trait DataType extends Tree }
Tree
- Une classe abstraite dont tous les arbres héritent.
Tree.pos
- Une méthode qui retourne la position à laquelle se trouve le première lexème correspondant à ce noeud de l'arbre.
Tree.setPos
-
Une méthode qui permet de définir la position du noeud. Elle retourne le noeud lui-même afin de permettre de l'utiliser «en ligne», comme dans
f(myTree setPos(34))
qui appellef
avec le noeudmyTree
dont la position à été définie simultanément. Notez qu'une telle fonction ne peut être définie en Java. DataType
- Une classe abstraite dont tous les arbres représentants des types héritent.
Un canevas pour cet objet est à votre disposition.
Classe du générateur d'arbre
Pour la génération de l'arbre, il faut modifier la classe Parser
de l'étape précédente pour générer l'arbre. L'interface de la classe modifiée sera la suivante.
class Parser(in: java.io.InputStream) extends Scanner(in) { def parse: Tree }
parse
- Une méthode qui va analyser (par rapport à la grammaire Vier) le flux de lexèmes du scanner et retourner l'AST correspondant.
Classe de l'imprimeur
Si vous définissez l'imprimeur comme une classe, vous pouvez lui donner l'interface suivante.
class Printer(out: java.io.PrintWriter) { def print(tree: Tree): Printer }
print
-
Un méthode qui imprime l'arbre
tree
sur la sortieout
. De manière similaire àsetPos
, celle-ci retourne une instance dePrinter
, ce qui permet d'écrire des appels chaînés commeprint(…).print(…).print(…)
.
Vous aurez intérêt à définir d'autres méthodes print…(…)
pour imprimer d'autres éléments utiles, comme des listes d'arbres (paramètres des fonctions) ou des chaînes de caractères.
Pour gérez l'indentation, définissez une méthode println
qui sache indenter de la bonne longueur en fonction de l'état de votre imprimeur. Changez l'état (indentez ou dé-indentez) à l'aide de fonctions spéciales utilisants le même système que print
pour pouvoir être chaînées.
Un canevas pour cette classe est à votre disposition.
Autres classes de support
PrinterTest.scala
-
Cet objet permet de tester la génération d'AST et votre imprimeur, à condition que l'interface de ceux-ci corresponde. A l'exécution
- son premier paramètre est le fichier source du programme à analyser,
- ses paramètres suivants, optionnels, sont une liste de fichiers dans lequel l'imprimeur écrira le résultat de l'analyse du fichier précédent (voir le conseil sur comment “déverminer l'analyse”).
Conseil d'implantation
Résistez à la tentation de changer le code de votre analyseur syntaxique. Ça n'est normalement pas nécessaire et le risque de réintroduire des erreurs est réel. A partir de votre analyseur syntaxique sans génération d'arbre, contentez-vous d'ajouter une valeur de retour a chaque méthode correspondant à la définition d'un non-terminal.
Essayez d'être précis sur les types que vous utilisez. La méthode correspondant à la définition du non-terminal “type”, par exemple, retournerait non pas n'importe quel arbre, mais un DataType
. De cette façon, vous détecterez facilement les erreurs conceptuelles ayant trait à “quoi est généré où”.
Valider l'analyse
Dans cette étape, il existe un moyen très efficace de tester votre compilateur. Il suffit de réinjecter le résultat de l'impression dans le compilateur, et de comparer le premier et le second programme écrit. S'ils sont rigoureusement identiques (au caractère), votre système est “stable”, ce qui est une propriété importante.
La classe PrinterTest
permet facilement de faire ce test. Son premier argument est le fichier Vier à utiliser, mais ses arguments suivants sont une série de fichiers dans lesquels le compilateur écrira le résultat de l'analyse du fichier précédent. Si vous exécutez le test comme scala vierc.PrinterTest fibonacci.vier temp1.vier temp2.vier
, les fichiers temp1.vier
et temp2.vier
doivent être égaux. Vous pouvez tester ça en exécutant diff temp1.vier temp2.vier
Attention! Quoique important, cette propriété ne garantit pas la correction de votre système.
Pour le déverminage des erreurs que cette méthode pourrait faire apparaître, il est utile de continuer à utiliser les méthodes ruleOut
et ruleIn
décrite dans le parser.