TP 4 - Parser

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.

Règles du parser

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
          ;

Actions et attributs

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.

Token Attributes

  • text: string - Le texte correspondant au jeton; peut être traduit dans un appel de fonction getText(). Exemple: $ID.text.
  • type: number - Le type du jeton (nombre entier différent de 0), tel que INT; peut être traduit dans un appel de fonction getType(). Exemple: $ID.type.
  • line: number - Le numéro de la ligne ou le jeton peut etre trouvé, commençant par 1; peut être traduit dans un appel de la fonction getLine(). Exemple: $ID.line.
  • pos: number - La position ou le premier caractère du jeton se trouve dans une ligne, commençant par 0; peut être traduit dans un appel de la fonction togetCharPositionInLine(). Example: $ID.pos.
  • index: number: L'indice général d'un token dans le flux des jetons, commençant par 0; peut être traduit dans un appel de la fonction getTokenIndex(). Example: $ID.index.
  • int: number - La valeur entière du texte d'un jeton, supposant que le texte est une valeur numerique valide; peut être traduit dans un appel de la fonction Integer.valueOf(text-of-token). Example: $INT.int.

Génération du parser

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.

Utilisation 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();

Visualisation de l'arbre d'analyse

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:

  1. Créez un nouveau projet en Visual Studio Code et assurez vous qu'il est le seul dossier ouvert
  2. Ajoutez le fichier qui contient la grammaire
  3. Créez un fichier avec l'extension .txt pour y ajouter les instructions que la grammaire doit analyser
  4. Créez un dossier spécial, appelé .vscode
  5. Dans le dossier .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
            }
        ]
    }
  6. Entrez dans le fichier de grammaire et ajoutez un Breakpoint (il faut juste appuyer sur le point rouge derrière le numéro de la ligne) breakpoint.jpg
  7. Dans la Palette de Commandes de la partie gauche, entrez sur la 4ème option (Debugger, indiqué par la flèche rouge dans l'image) et démarrez le Debugger en appuyant sur le bouton indiqué par la flèche verte de l'image suivante: start-debugger.jpg
  8. Maintenant, la visualisation du 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: step-into-tree.jpg
  9. Au fur et à mesure que vous appuyez sur Step Into, vous allez observer la génération de l'arbre d'analyse (flèche rouge), la consommation des jetons (flèche verte) et l'appel des règles dans la pile (flèche bleue) parse-tree-generation.jpg
  10. A la fin de l'exécution, le Parse Tree aura la structure suivante: parse-tree.jpg

Exercices

  1. Suivez le tutoriel précédent (celui avec la déclaration d'une variable simple) pour la visualisation de l'arbre d'analyse. (1p)
  2. Ajoutez dans la grammaire les instructions nécessaires pour la déclaration complexe des variables, selon le langage suivant: (2p)
    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.

  3. Ajoutez à votre grammaire des jetons et des règles pour les expressions mathématiques. Votre grammaire doit reconnaître les opérations et les opérateurs d'addition, soustraction, multiplication et division (+, -, *, /, %). Suivez les étapes nécessaires pour visualiser l'arbre d'analyse et testez la fonctionnalité du programme pour l'expression 2+5/10-7. (3p)
  4. Repétez l'exercice précédent en tenant compte du fait qu'une expression peut aussi contenir des variables. Comme mentionné dans le premier exercice, les variables sont de la forme _var1. Vérifiez la correctitude de la grammaire en ajoutant l'expression 15/_var1-5; au fichier texte pour les données.(1p)
  5. Ajoutez à la grammaire les jetons et les règles nécessaires pour accépter des parantheses à l'intérieur des expressions. Suivez les étapes nécessaires pour la visualisation de l'arbre et testez le programme en ajoutant l'expression (2+5)/3 au fichier texte pour les données. (1p)
  6. Faites la grammaire accepter plus d'une expression, supposant que les expressions sont séparées par ;, 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;
  7. BONUS: Changez la grammaire pour que les variables puissent être déclarées en utilisant des expressions et visualisez l'arbre d'analyse pour l'instruction suivante: (1.5p)
    int _var1 = 2 + 3 * 5;
alf/laboratoare/04.txt · Last modified: 2021/03/26 19:43 by diana.ghindaoanu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0