Description de l'étape

Vous devez écrire les trois éléments suivants.

  1. Vous devez définir un type de données (AST) qui corresponde à la syntaxe abstrait de la spécification de Vier.
  2. 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).
  3. 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 appelle f avec le noeud myTree 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 sortie out. De manière similaire à setPos, celle-ci retourne une instance de Printer, ce qui permet d'écrire des appels chaînés comme print(…).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.