This is an old revision of the document!
Le but de ce TP est d'introduire la notion de parser
en ANTLR4.
Un parser représente la partie d'un compilateur qui reconnaît des jetons (tokens) ordonnés et spécifiés par un ensemble de règles d'analyse. Cet ensemble de règles décrit un “langage” source reçu par le parser sous la forme d'instructions séquentielles du programme source, des entrées de la ligne de commande, de balises ou d'autres.
L'analyseur généré fait correspondre les jetons d'entrée (tokens définis par le lexer) aux règles, émettant un arbre de syntaxe abstraite (AST), dont on va discuter plus dans les TPs suivants. Cet arbre représente la sortie de l'analyseur.
La règle la plus simple est juste un nom de règle suivi d'une seule alternative terminée par un point-virgule, comme on l'a vu dans le TP précédent:
grammar Alf; start:;
Lorsqu'une règle se compose de plusieurs aternatives, on utilise le séparateur |
pour les différencier:
grammar Alf; start: expression ';' ; expression: addition | subtraction | multiplication | division ;
Les actions représentent des blocs de texte écrits dans le langage de programmation cible et délimités par des accolades {}
. Elles sont déclenchées selon leur position dans la grammaire.
grammar Alf; declaration: type VARIABLE ';' { console.log('This is a declaration statement!'); } ; type : 'string' | 'number' | 'boolean' ;
Dans la plupart des cas, les actions sont utilisées pour accéder aux attributs des tokens et aux références des regles qu'on a définies. Par exemple, dans la grammaire suivant, on va afficher pour chaque type d'expression la valeur numérique des opérants et le token pour l'opérateur:
grammar Alf; start: expression ; expression: left=expression op=MUL right=expression {if($left.text && $op.text && $right.text) console.log($left.text + ' ' + $op.text + ' ' + $right.text + ' ');} | left=expression op=DIV right=expression {if($left.text && $op.text && $right.text) console.log($left.text + ' ' + $op.text + ' ' + $right.text + ' ');} | left=expression op=PLUS right=expression {if($left.text && $op.text && $right.text) console.log($left.text + ' ' + $op.text + ' ' + $right.text + ' ');} | left=expression op=MINUS right=expression {if($left.text && $op.text && $right.text) console.log($left.text + ' ' + $op.text + ' ' + $right.text + ' ');} | INT ; WS : ([' ']+) -> skip; /* Skip whitespaces */ NEWLINE : ([\r\n]+); INT : ([0-9]+) ; MUL : ('*'); DIV : ('/'); PLUS : ('+'); MINUS : ('-');
Tous les jetons (tokens) incluent un ensemble d'attributs prédéfinis, read-only, qui décrivent des propriétés utiles pour les tokens et auxquels les actions peuvent accéder a travers l'instruction $label.attribute
, où label représente le nom du jeton/de la règle et attribute le nom de l'attribut.
getText()
. Exemple: $ID.text.getType()
. Exemple: $ID.type. getLine()
. Exemple: $ID.line.togetCharPositionInLine()
. Example: $ID.pos.getTokenIndex()
. Example: $ID.index.Integer.valueOf(text-of-token)
. Example: $INT.int.
Après avoir décrit une grammaire dans un fichier .g4
avec des jetons et des règles, il faut exécuter la commande suivante:
npm run antlr4ts
Cette commande va générer les fichiers .ts
et .js
correspondants, dont on va se servir pour diviser un texte en jetons et pour l'analyser a l'aide du parser.
Un exemple d'utilisation du parser dans un fichier index.ts
:
import { CharStreams, CodePointCharStream, CommonTokenStream, Token } from 'antlr4ts'; import { AlfLexer } from './AlfLexer.js'; import { AlfParser } from './AlfParser.js'; import { AlfListener } from './AlfListener.js'; import { AlfVisitor } from './AlfVisitor.js'; import { ParseTree } from 'antlr4ts/tree/ParseTree'; import { AbstractParseTreeVisitor } from 'antlr4ts/tree/AbstractParseTreeVisitor'; let input: string = "1+2*4/5"; let inputStream: CodePointCharStream = CharStreams.fromString(input); let lexer: AlfLexer = new AlfLexer(inputStream); let tokenStream: CommonTokenStream = new CommonTokenStream(lexer); let parser: AlfParser = new AlfParser(tokenStream); // Parse the input, where `start` is whatever entry point you defined let tree = parser.start();
Dans ce TP on va ce concentrer sur la visualisation de l'arbre et sur sa génération pas à pas, pour comprendre comment le parser fonctionne.
La structure de la grammaire peut etre visualisée d'une manière très intuitive à l'aide de l'extension d'ANTLR4 pour VSCode qui a été installée dans le TP précédent. On aura besoin aussi d'un fichier de configuration, qui nous aidera aussi à la vérification des erreurs et au processus de debug.
Pour pouvoir configurer votre projet de sorte que vous puissiez visualiser la construction progressive de l'arbre d'analyse, vous devez suivre les étapes ci-dessous:
.txt
pour y ajouter les instructions que la grammaire doit analyser.vscode
, créez un fichier de configuration launch.json avec la structure suivante: { "version": "0.2.0", "configurations": [ { "name": "antlr4", "type": "antlr-debug", "request": "launch", "input": "sample.txt", /* Le nom du fichier ou vous allez stocker les données de test */ "grammar": "Alf.g4", /* Le nom du fichier de grammaire */ "startRule": "start", /* Le nom de la première règle de la grammaire */ "printParseTree": true, "visualParseTree": true } ] }
Parse Tree
devrait s'ouvrir à droite de la fenetre de VSCode. Pour voir la génération progressive de votre arbre d'analyse, vous devez appuyer sur le bouton Step Into, marqué par la fleche rouge dans l'image suivante: int _var1 = 2; float _var2 = 5.55; string _var3 = "alf";
Suivez les étapes nécessaires pour visualiser l'arbre d'analyse et testez chaque règle à l'aide du fichier texte pour les données.
2+5/10-7
. (3p)_var1
. Vérifiez la correctitude de la grammaire en ajoutant l'expression 15/_var1-5;
au fichier texte pour les données.(1p)(2+5)/3
au fichier texte pour les données. (1p);
, mais aussi par des lignes vides. Modifiez le contenu du fichier texte pour les données en ajoutant les expressions suivantes et visualisez l'arbre d'analyse: (2p) string _var1 = "alf"; (2+7)/5*3;
int _var1 = 2 + 3 * 5;