La construction d'un analyseur lexical avec Tatoo s'effectue en plusieurs étapes :
Les règles de l'analyseur lexical sont décrites dans un fichier XML, .xlex, dont la syntaxe est précisée par la DTD, lexer.dtd.
Chaque règle est définie dans un élément XML <rule id="idenficateur">definition</rule> dont l'attribut id précise l'identificateur unique.
Si l'on ajoute, dans l'élément rule, l'attribut beginning-of-line="true", cela indique que la règle doit être reconnue en début de ligne.
Un élément rule contient un sous-élément <main>expression</main> qui précise l'expression rationelle principale de la règle puis, éventuellement, une contrainte sur ce qui suit. Cette contrainte peut être soit :
Dans tous les cas, la chaîne de caractères correspondant à la contrainte ne fera pas partie du lexème reconnu par cette règle.
Les éléments simples des expressions rationnnelles sont :
Les caractères non imprimables sont codés, comme en Java, par un chaîne de caractères commençant par \u suivi du code du caractère en hexadicimal.
Les opérateurs classiques sont définis sur les expressions rationnelles :
En dehors des règles, il est possible de définir des macro-expressions rationnelles avec l'élément <define-macro name="name">expression </define-macro>. Ces macros peuvent ensuite être utilisées à la place des expressions rationnelles auxquelles elles correspondents grâce à l'élément <macro name="name">
Un exemple de fichier de règles (Calc.xlex) est donné ci-dessous. Il décrit les règles des expressions arithmétiques.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE lexer SYSTEM
"http://igm.univ-mlv.fr/~jcervell/DTD/lexer.dtd">
<lexer>
<!-- Macros -->
<define-macro name="number">
<plus>
<interval from="0" to="9"/>
</plus>
</define-macro>
<define-macro name="separator">
<set>
<letter value=" "/> <letter value="\t"/>
<letter value="\n"/> <letter value="\r"/>
</set>
</define-macro>
<define-macro name="separators">
<star>
<macro name="separator"/>
</star>
</define-macro>
<!-- Rules-->
<rule id="value">
<main>
<macro name="number"/>
</main>
</rule>
<rule id="separator">
<main>
<macro name="separator"/>
</main>
</rule>
|
<rule id="plus">
<main>
<cat>
<macro name="separators"/>
<letter value="+"/>
<macro name="separators"/>
</cat>
</main>
</rule>
<rule id="minus">
<main>
<cat>
<macro name="separators"/>
<letter value="-"/>
<macro name="separators"/>
</cat>
</main>
</rule>
<rule id="star">
<main>
<cat>
<macro name="separators"/>
<letter value="*"/>
<macro name="separators"/>
</cat>
</main>
</rule>
<rule id="equals">
<main>
<cat>
<macro name="separators"/>
<letter value="="/>
<macro name="separators"/>
</cat>
</main>
</rule>
</lexer>
|
Pour générer les implantations des automates qui correspondent à un ensemble de règles décrites dans un fichier .xlex, il faut exécuter l'application Tatoo contenue dans le fichier Jar exécutable tatoo.jar. Cette application prend en argument le fichier .xlex et produit, dans le répertoire courant, une énumération qui a pour nom lexer.RuleEnum. Diverses options permettent de changer ce comportement par défaut.
Ces options sont :
Par exemple, la commande suivante génère, à partir des règles décrites dans le fichier Calc.xlex, une énumération de nom CalcRules qui fait partie du paquetage fr.umlv.compil. Les sources générées sont placées dans le répertoire generated du répertoire courant.
java -jar tatoo.jar -d ./generated/ -p fr.umlv.compil -n rule=CalcRules Calc.xlex
La classe produite est placée dans le fichier CalcRules.java.
Les valeurs de l'énumération produite correspondent aux identificateurs des règles spécifiés dans le fichier .xlex. Les implantations des automates correspondant aux expressions rationnelles des règles, ainsi que les autres informations des règles, sont associées aux valeurs de l'énumération.
Ces informations sont retournées, pour chaque valeur de l'énumération par les méthodes suivantes, décrites dans l'interface Rule implantée par l'énumération :
Vous ne devriez pas avoir besoin d'utiliser directement ces méthodes.
Les implantations des automates générées utilisent et sont utilisées par des classes de Tatoo (Rule en particulier). Il faut donc ajouter le fichier tatoo-runtime.jar au classpath au moment de la compilation et de l'exécution de tout programme les utilisant.
javac -classpath tatoo-runtime.jar MonProgramme.java |
java -cp tatoo-runtime.jar MonProgramme |
Les automates générés sont utilisés au sein d'un analyseur lexical de la classe Lexer<R extends Rule, B extends CharacterBuffer>. Le type R correspond au type des règles utilisées par l'analyseur lexical et B au type du tampon utilisé par l'analyseur.
L'analyseur est construit par une des méthodes statiques createLexer() de la classe Lexer. La méthode la plus simple prend en arguments, au moment de sa construction :
Par exemple, la portion de code suivante construit un analyseur lexical, paramétré par les règles de l'énumaration CalcRules et les lexèmes produits sont de type CharSequence :
Lexer<CalcRules, ReaderWrapper> lexer = Lexer.createLexer(buffer, listener, CalcRules.values());
Cet analyseur lexical travaille sur un flot de caractères accessible au travers de buffer, il informe l'objet listener chaque fois qu'il reconnait un lexème et l'ensemble des règles qu'il utilise est l'ensemble des régles définies dans CalcRules.
Le texte à analyser est représenté par un objet qui implante l'interface LexerBuffer. Pour l'instant, la classe ReaderWrapper est la seule implantation de cette interface. Cette classe permet d'envelopper un objet implantant l'interface Reader pour le passer à l'analyseur lexical.
Par exemple, la portion de code suivante construit une enveloppe pour le fichier Calc.txt :
ReaderWrapper buffer = new ReaderWrapper(new FileReader("Calc.txt"));
L'observateur de lexèmes implante l'interface LexerListener<R,B> qui contient l'unique méthode ruleVerified(). Cette méthode est appelée chaque fois que l'analyseur reconnait un lexème. Cette méthode reçoit en argument :
Par défaut, les caractères reconnus sont conservés dans le tampon tant qu'ils n'ont pas été supprimés explicitement, par la méthode discard(). La méthode view() retourne une vue de tous les caractères reconnus et non encore supprimés.
Cette dernière méthode retourne des valeurs qui sont susceptibles de changer si la méthode discard() est appelée.
Par exemple, la classe suivante affiche les entiers trouvés dans une expression arithmétique décrite dans les règels du fichier Calc.xlex :
LexerListener<CalcRules, TokenBuffer> listener = new LexerListener<CalcRules, TokenBuffer>() {
public void ruleVerified(CalcRules t, int lastTokenLength, TokenBuffer buffer) {
switch (t) {
case value:
System.out.println(buffer.view());
buffer.discard();
break;
default:
buffer.discard();
}
}
};
Au moment de la construction de l'analyseur syntaxique il est possible de passer, à la place d'un tableau de règles, un objet implantant l'interface RuleActivator<R>. Sa méthode activateRules() est appelée avant de commencer à reconnaître un nouveau lexème. Elle permet de changer l'ensemble des règles actives ; c'est-à-dire que l'analysuer tente de reconnaître.
Lexer<CalcRules, ReaderWrapper> lexer = Lexer.createLexer(buffer, listener, activator);
L'interface RuleActivator<R> contient une unique méthode activateRules() qui reçoit en argument l'ancien ensemble des règles actives et qui doit retourner le nouveau.
Par exemple, l'activateur de règles suivant assure que le texte reconnu commence par une valeur suivie du lexème =, puis n'importe quel lexème sauf =.
final EnumSet<CalcRules> valueSingleton = EnumSet.of(CalcRules.value);
final EnumSet<CalcRules> equalsSingleton = EnumSet.of(CalcRules.equals);
final EnumSet<CalcRules> notEqualsSet = EnumSet.complementOf(equalsSingleton);
RuleActivator<CalcRules> activator = new RuleActivator<CalcRules>() {
public Iterable<CalcRules> activateRules(Iterable<CalcRules> oldRules) {
if (oldRules == null) {
return valueSingleton;
}
if (oldRules == valueSingleton) {
return equalsSingleton;
}
return notEqualsSet;
}
};
L'analyse d'un nouveau caractère de la séquence est réalisé par l'appel à la méthode step() de l'analyseur lexical. Pour réaliser l'analyse total d'une séquence, il faut donc l'appeler, tant qu'il y a des caractères dans la séquence. À la fin de l'analyse il faut appeler la méthode flush() de l'analyseur pour être sûr que le dernier lexème est bien reconnu.
Pour savoir s'il y a encore des caractères dans la séquence il faut appeler la méthode notEof() de la classe ReaderWrapper.
while(buffer.notEof()) {
lexer.step();
}
lexer.flush();