Construire ses parsers ANTLR avec Maven - Partie 2/3 - Avec Maven

le 12/12/2008 par Thomas Vial
Tags: Software Engineering

Dans la première partie, nous avons vu le principe de fonctionnement d'ANTLR : un fichier de description de grammaire Demo.g --> deux fichiers DemoLexer.java et DemoParser.java).

Supposons maintenant que nous ayons un projet Java, dont un composant est le langage spécifique à implémenter, et que ce projet soit construit avec Maven 2. L'enseignement important de la première partie, c'est qu'ANTLR va générer pour nous du code source Java, destiné à faire partie intégrante de notre projet. Dès lors, toute modification du fichier Demo.g devra faire l'objet d'une regénération du lexer et du parser afin d'être prise en compte dans le code qui utilise la grammaire. L'idéal est bien sûr d'intégrer cette phase de génération au cycle de build existant du projet...

C'est ce que nous allons détailler dans cette partie de la série.

NB : un projet de démonstration complet et fonctionnel est disponible à cette adresse, avec une grammaire un peu plus évoluée que l'exemple précédent. Le code source montre aussi comment appeler le parser depuis le programme principal, intégration qui n'est pas détaillée dans cet article. N'hésitez donc pas à récupérer et examiner ce projet en détail !

Le principe

Il est des plus naturels : utiliser un plugin Maven, qui va trouver toutes les grammaires du projet - les fichiers *.g - et générer les lexers et parsers correspondant à chacune.

La version 3.1.1 toute récente d'ANTLR (1er octobre 2008) inclut une mise à jour du plugin antlr3-maven-plugin. La version précédente existait depuis un moment, mais n'était compatible qu'avec les versions 3.0.x d'ANTLR, qui souffraient d'une limitation très importante pour nous : l'absence de code de retour de la phase de génération. En clair, si le fichier de description de la grammaire (.g) était invalide, le plugin affichait des messages d'erreur mais le build continuait comme si de rien n'était...

Ce problème est maintenant réglé, et nous pouvons utiliser le plugin actuel dans toutes les situations avec confiance : lancement manuel, build continu, ...

Il vous faut une version récente de Maven, et un JDK 1.5 dont dépend antlr3-maven-plugin.

Arborescence du projet

Voici l'arborescence qui a été retenue pour ce projet :

./
    pom.xml
src/
    main/
        antlr/
            com/
                octo/
                    mvnantlr/
                        Demo.g
        java/
            com/
                octo/
                    mvnantlr/
                            Main.java
    target/
        classes/
            (Fichiers .class compilés)
        generated-sources/
                com/
                    octo/
                        mvnantlr/
                            (Fichiers .java des lexers et parsers)

Notez que par défaut, les fichiers sources Java générés du lexer et du parser ne sont pas avec les autres classes du projet : ils sont sous target/ avec les éléments "volatils" du build. Avec cette configuration, ils ne seront donc pas soumis au contrôle de source (ex. SVN) et seront effacés avec mvn clean.

Vous remarquerez aussi que le fichier Demo.g est à l'intérieur d'une arborescence de type "package" : com/octo/mvnantlr/, tout comme les fichiers qu'il génère sous generated-sources, et ce n'est pas un hasard. Au moment d'écrire les fichiers Java, le plugin reconstitue en effet le chemin (relatif à src/) où il avait trouvé la source .g. Une conséquence importante est que, sous src/antlr/, chaque fichier .g doit être situé dans un répertoire compatible avec les clauses @parser::header et @lexer::header qu'il contient.

Configuration du POM du projet

Nous commençons par modifier le fichier pom.xml du projet qui contient notre grammaire, pour déclarer l'appel au plugin.

Voici les fragments importants du POM :

<project>

    ...

    <dependencies>
        <!-- Dépendance vers ANTLR v3.1.1 -->
        <dependency>
            <groupid>org.antlr</groupid>
            <artifactid>antlr</artifactid>
            <version>3.1.1</version>
        </dependency>
    </dependencies>

    ...

    <build>
        <plugins>
            <!-- Force la compilation en Java 1.5 -->
            <plugin>
                <groupid>org.apache.maven.plugins</groupid>
                <artifactid>maven-compiler-plugin</artifactid>
                <configuration>
                    <source>1.5</source>
                    <target>1.5</target>
                </configuration>
            </plugin>

            <!-- Appel du plugin ANTLR -->
            <plugin>
                <groupid>org.antlr</groupid>
                <artifactid>antlr3-maven-plugin</artifactid>
                <version>3.1.1.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>antlr</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

La phase de déclenchement d'antlr3-maven-plugin n'est pas spécifiée : il se branche tout seul sur la phase generate-sources, qui est l'étape idéale du cycle de build pour une tâche de construction de fichiers Java. On ne précise pas non plus les fichiers .g à transformer, il va les chercher en parcourant l'arborescence sous src/antlr/ (par défaut), et confronter leur date de dernière modification à celle des fichiers résultants s'ils existent déjà.

On aurait pu aussi passer au plugin le paramètre outputDirectory (dans son tag ), pour lui faire écrire les fichiers Java du lexer et du parser sous le répertoire src/ du projet, contrairement au comportement par défaut qui est de les mettre dans target/. C'est utile si on veut les faire versionner par le gestionnaire de source en même temps que les autres fichiers.

Un essai !

Maintenant, un mvn install devrait vous (re)générer les classes du lexer et du parser, en émettant au passages quelques warnings signalés par ANTLR et que vous pouvez ignorer. Vous pouvez aussi essayer de modifier le fichier Demo.g en introduisant une faute de syntaxe volontaire, ni vu ni connu - enfin si, justement, le build échouera comme il se doit !

Si vous travaillez sur le projet de démonstration, j'y ai mis par commodité un appel supplémentaire, branché sur la phase integration-test, pour compiler et lancer notre petite application en un seul appel (mvn integration-test ou mvn install). Voici quelques exemples de commandes que le programme accepte au clavier :

// Commentaire
print 1+2*(4-15)/7;   // Evaluation d’expressions arithmétiques simples sur des entiers
a=3*4-2/3;            // Affectation de variables
debug;                // Affichage de la table des variables
quit                  // Sortie du programme

Pas vraiment le langage pionnier de la prochaine révolution informatique, mais c'est un premier pas :-)

Une note sur la mémoire

Bien sûr cela ne voit pas sur un exemple aussi simple, mais ANTLR consomme beaucoup de ressources mémoire lors de la traduction des fichiers .g. Avec des grammaires complexes, comportant beaucoup de tokens et des règles savamment imbriquées, il peut être nécessaire d'augmenter la mémoire allouée au lancement de Maven (options -Xms et -Xmx de la JVM).

Et pour la suite...

Ne manquez pas non plus la troisième et dernière partie de l'article, à venir, qui montrera comment écrire des tests unitaires clairs et concis pour vos grammaires... et bien sûr comment faire en sorte qu'ils s'intègrent eux aussi au processus de build que nous avons commencé ici.

En attendant, si vous voulez découvrir les richesses inépuisables d'ANTLR proprement dit, vous pouvez visiter le wiki à cette adresse : http://www.antlr.org/wiki/display/ANTLR3/ANTLR+3+Wiki+Home