Académique Documents
Professionnel Documents
Culture Documents
l’aide de SableCC
Écrire une calculatrice est une tâche simple - il suffit d’ajouter neuf boutons étiquetés de 1 à
9 et d’ajouter un bouton plus et moins et nous sommes presque prêts à partir. Dans cette
entrée, je vais écrire une calculatrice appelée SimpleCalc qui n’a pas d’interface graphique,
mais qui prendra une expression arbitraire et en calculera les résultats. L’entrée que
j’utiliserai comme objectif immédiat est la suivante:
25-37+2*(1.22+cos(5))*sin(5)*2+5%2*3*sqrt(5+2)
Selon Google,le résultat est -9.83033875. Certains des sujets délicats que nous devrons
traiter sont lapriorité des opérateurs (multiplication avant addition, etc.), les expressions
imbriquées (2*1.22+cos(5) != 2*(1.22+cos(5))) etl’associativité (5+7 == 7+5 & 7-5 != 5-7 etc.).
En bref, SableCC peut être utilisé pour générer automatiquement le code de l’analyseur
utilisé dans n’importe quel compilateur, ainsi que dans de nombreux autres cas où l’entrée
doit être analysée - comme dans ce cas. SableCC lui-même est écrit en Java parEtienne M.
Gagnonet lecode sourceest disponible gratuitement.
La sortie standard de SableCC est du code Java. Ainsi, l’analyseur que nous sommes sur le
point de générer sera transformé en un certain nombre de fichiers Java que nous pourrons
incorporer dans notre propre code source et étendre. Comme je vais écrire la calculatrice en
C #, je préférerais de loin travailler directement avec les fichiers sources C #, plutôt que
d’avoir à porter la sortie Java ou à l’appeler par d’autres moyens. Heureusement,Indrek
Mandrea créé une variante de SableCC qui générera l’analyseur en Java, C #, C ++,
O’Caml, Python, C, Graphviz Dot ou XML. Tout ce que nous avons à faire est de télécharger
le fichier zipsablecc-3-beta.3.altgen.20041114à partir de la page d’accueil de la page
SableCC d’Indrek. Une fois téléchargé et décompressé, nous pouvons l’exécuter, tant que
Java est installé. Créez d’abord un fichier bat avec le contenu suivant :
Assurez-vous de remplacer mon chemin par le chemin dans lequel vous avez extrait le
paquet altgen SableCC. Lorsque nous invoquerons SableCC à partir de maintenant, nous le
ferons via ce fichier bat que j’ai choisi d’appelersablecc_altgen.bat, juste pour simplifier la
syntaxe.
Définition de la grammaire
Pour que SableCC puisse générer notre analyseur, nous devons d’abord définir le langage
qu’il doit prendre en charge. La façon dont nous le faisons est de définir la langue au format
(E)BNF. Je n’écrirai pas de tutoriel générique sur la façon d’écrire des grammeurs dans
SableCC car il y en a déjà un certain nombre de bons sur la page de documentation
deSableCC, ainsiqu’un de Nat Pryce qui n’estpas sur la page de documentation. Enfin, il y a
aussi uneliste de diffusion, bien que l’activité soit limitée.
Nous allons commencer par créer un nouveau fichier texte appelésimplecalc.sablecc, c’est là
que nous allons définir notre grammaire SimpleCalc. De plus, j’ai créé un nouveau fichier bat
appelé simplecalc_sable.bat avec le contenu suivant:
cls
Le fichier bat ci-dessus appellera celui que nous avons créé précédemment. -d
généréspécifie le nom du répertoire de sortie. -t csharpspécifie le type de code source de
sortie, C# dans ce cas. Le dernier argument est le nom du fichier sablecc d’entrée. À partir
de maintenant, nous pouvons simplement lancersimplecalc_sablepour démarrer le
processus de compilation SableCC.
Je vais d’abord publier le contenu complet du fichier simplecalc.sablecc, puis parcourir les
sections spécifiques une par une.
Package SimpleCalc;
Helpers
Tokens
add = '+';
sub = '-';
mul = '*';
div = '/';
mod = '%';
sqrt = 'sqrt';
cos = 'cos';
sin = 'sin';
lparen = '(';
rparen = ')';
Productions
exp
| {paren} exp
| {sqrt} exp
| {cos} exp
| {sin} exp
| {number} number
Package SimpleCalc;
La déclaration Package définit simplement le nom du package global. Si cela est exclu (ce
qui est valide selon SableCC), nos namesapces dans le code C# généré seront vides et
donc invalides.
Helpers
Les assistants sont essentiellement des espaces réservés que vous pouvez configurer et
utiliser dans le fichier SableCC. Ils n’ont pas de signification ou de fonctionnalité plus
profonde, c’est juste un moyen de pouvoir facilement exprimer du code commun par son
nom. Comme nous ferons référence auxchiffresplusieurs fois, il est utile de le définir comme
un assistant au lieu de répliquer ['0' .. '9']plusieurs fois dans le code. ['0' .. '9']signifie tous
les chiffres compris entre 0 et 9.
Tokens
add = '+';
sub = '-';
mul = '*';
div = '/';
mod = '%';
sqrt = 'sqrt';
cos = 'cos';
sin = 'sin';
lparen = '(';
rparen = ')';
Notez que je saute en avant et ignore la sectionProductionspour l’instant, j’y reviendrai dans
un instant. L’arborescence syntaxique abstraitedéfinit les nœuds qui seront présents dans
notre AST analysé. Chaque type d’opération et de fonction a un nœud correspondant dans
l’AST. Ainsi, et l’opération add consistera en un nœud Ajouter avec deux enfants - une
expression gauche et une expression droite. Ces expressions peuvent elles-mêmes être des
nombres constants ou des expressions imbriquées - puisqu’elles sont définies comme exp
qui est une référence récursive au typeexpAST réel.
Add, sub, mul, divetmodsont des opérateursbinaireset ont donc deux expressions enfants.
Paren, sqrt, cosetsin (et à certains égards, nombre) sont des opérateurs unaires en ce sens
qu’ils n’ont qu’un seul enfant/paramètre - une expression. Le nombre est un nœud feuille qui
exprime une constante numérique réelle.
Productions
La première production que nous définissons est une production générique pour exprimer
des expressions. Notez que la façon dont nous définissons la priorité des opérateurs
consiste d’abord à exprimer l’opérateur le moins prioritaire (add&sub), puis à référencer les
opérations factorielles (mul,div,mod&unary) et ainsi de suite. exp {-> exp}signifie que la
syntaxe concrète d’une expression est également mappée dans le nœud de l’arbre
syntaxique abstrait appelé « exp ». ProductionsetAbstract Syntax Treesont deux espaces
de noms différents et peuvent donc partager les mêmes noms.
L’opération add est définie par une expression gauche suivie du jeton add (défini comme '+'
précédemment), puis d’une expression de facteur à droite, définissant ainsi la relation de
priorité entre l’opérationaddet les opérations factor. Enfin, nous définissons que l’opération
add mappe dans une nouvelle instance du nœud exp AST, ayant les expressions gauche et
droite comme paramètres (enfants dans l’AST). La sous-opération est presque identique à
l’opérateur add.
Toutes les expressions de facteurs sont simplement mappées sur la production factorielle
définie ultérieurement.
La plus simple de toutes les expressions estl’expression numériqueunaire qui définit une
constante numérique. L’expression number est mappée dans un nouveau nœud AST du type
exp.number, avec le nombre réel comme paramètre. Les fonctionssqrt, cosetsindéfinissent
toutes l’entrée comme le nom de la fonction et l’expression de paramètre entre parenthèses.
Enfin, nous définissons la fonction {paren}unaire qui est une expression arbitraire entre
parenthèses. Cela est mappé dans le type exp.paren AST, en prenant l’expression arbitraire
comme paramètre. La fonction {paren} nous permet de différencier des expressions comme
« 5*2-7 » et « 5*(2-7) ».
La production finale est ce qui nous permet d’enchaîner les expressions. Sans laproduction
exp_list, seules des opérations uniques seraient autorisées (5+2, 3*7 etc.), pas des chaînes
d’expressions (5+2+3, 5*2+3, etc.). exp_list {-> exp*}définit que la production exp_list est
mappée dans une liste d’exp dans l’AST.
Génération de l’analyseur
Une fois que nous avons défini la grammaire, nous sommes prêts à exécuter lefichier
simplecalc_sablebat, ce qui, espérons-le, donnera le résultat suivant:
Verifying identifiers.
State: INITIAL
- Constructing NFA.
..............................
- Constructing DFA.
...................................................
....................
..............................
..............................
..............................
..
..............................
Maintenant, si nous regardons dans le répertoire généré, il devrait y avoir six fichiers:
analysis.cs,lexer.cs,nodes.cs,parser.cs,prods.cs et tokens.cs. Les fichiers doivent
contenir des classes dans l’espace de nomsSimpleCalc.
using System;
using SimpleCalc.analysis;
using SimpleCalc.node;
namespace SimpleCalc
int indent;
Console.Write("".PadLeft(indent, 't'));
printIndent();
Console.ForegroundColor = ConsoleColor.White;
Console.Write(node.GetType().ToString().Replace("SimpleCalc
if (node is ANumberExp)
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine(" " + node.ToString());
else
Console.WriteLine();
printNode(node);
indent++;
indent--;
using System;
using System.IO;
using SimpleCalc.lexer;
using SimpleCalc.node;
using SimpleCalc.parser;
namespace SimpleCalc
class Program
if (args.Length != 1)
// Read source
// Parse source
try
ast = parser.Parse();
exit(ex.ToString());
// Print tree
ast.Apply(printer);
exit("Done");
if (msg != null)
Console.WriteLine(msg);
else
Console.WriteLine();
Console.Read();
Environment.Exit(0);
J’ai fait en sorte que le programme prenne un seul argument, un nom de fichier où notre
expression de calcul est écrite. En prenant un fichier comme paramètre, il m’est plus facile
de modifier l’expression directement dans Visual Studio sans avoir à configurer les
paramètres de lancement. Le seul paramètre de lancement qui doit être défini est l’argument
file.
En comparant l’AST imprimé avec l’expression d’entrée, nous verrons qu’ils correspondent à
la fois dans le contenu et en ce qui concerne la priorité des opérateurs. Il ne reste plus qu’à
effectuer le calcul réel de l’expression.
Calcul de l’expression basé sur l’arborescence
syntaxique abstraite
Ajoutez un nouveau fichier appeléAstCalculator.cset collez le contenu suivant :
using System;
using System.Collections.Generic;
using System.Globalization;
using SimpleCalc.analysis;
using SimpleCalc.node;
namespace SimpleCalc
get
if (result == null)
return result.Value;
if (stack.Count != 1)
result = stack.Pop();
// Associative operators
stack.Push(stack.Pop() * stack.Pop());
stack.Push(stack.Pop() + stack.Pop());
stack.Push(stack.Pop() - numB);
stack.Push(stack.Pop() % numB);
stack.Push(stack.Pop() / numB);
// Unary
stack.Push(Math.Sqrt(stack.Pop()));
stack.Push(Math.Cos(stack.Pop()));
stack.Push(Math.Sin(stack.Pop()));
stack.Push(Convert.ToDouble(node.GetNumber().Text.Trim(), n
}
Je ne vais pas passer en revue toutes les parties de la calculatrice car de nombreuses
fonctions sont très similaires. Je vais décrire les plus importants ci-dessous.
get
if (result == null)
return result.Value;
Comme tous les nombres sont traités comme des doubles, le résultat sera également un
double. Le résultat peut être récupéré via la propriétéCalculatedResult, mais seulement une
fois le calcul effectué - nous vérifions donc si le résultat est nul ou non.
Tout en parcourant l’AST pour effectuer les calculs, nous maintenons l’état grâce à
l’utilisation d’une pile générique de doubles.
if (stack.Count != 1)
result = stack.Pop();
Au début, la pile sera vide. Une fois que nous avons traversé l’arborescence, la pile ne
devrait contenir qu’un seul élément - le résultat. Pour nous assurer qu’il n’y a pas d’erreurs,
nous nous assurons que la pile ne contient qu’un seul élément, après quoi nous le renvoyons
en le faisant sortir de la pile.
L’opérateur unaire le plus important est probablement le nombre constant. Chaque fois que
nous sommes dans un nœudANumberExp, nous lisons le numéro et le poussons sur la pile.
stack.Push(Math.Sqrt(stack.Pop()));
Les autres opérateurs unaires suivent le même schéma. Nous faisons sauter la pile et
effectuons une opération mathématique sur la valeur popped, après quoi nous repoussons le
résultat sur la pile.
stack.Push(stack.Pop() * stack.Pop());
Les opérateurs associatifs sont simples en ce sens qu’ils n’ont aucune exigence quant à
l’ordre dans lequel se trouvent les paramètres d’entrée. En tant que tel, une multiplication
simple fait apparaître deux nombres de la pile et renvoie le résultat multiplié sur la pile.
stack.Push(stack.Pop() - numB);
Les administrateurs non associatifs doivent d’abord afficher un nombre et le stocker dans
une variable temporaire. La raison pour laquelle nous devons le faire est que nous travaillons
avec une pileFIFO, ce qui signifie que le deuxième nombre ne sera pas le plus haut de la pile
et que nous ne pouvons donc pas effectuer le calcul dans une seule expression.
Maintenant que nous avons créé la classe AstCalculator, il ne nous reste plus qu’à modifier
la méthode principale pour qu’elle exécute la calculatrice.
// Print tree
ast.Apply(printer);
// Calculate expression
ast.Apply(calculator);
Conclusion
J’ai maintenant montré comment nous pouvons définir une grammaire de langue dans
SableCC et lui faire générer automatiquement un analyseur pour nous. En utilisant
l’analyseur SableCC, nous pouvons lire une chaîne d’entrée et la transformer en un arbre
syntaxique abstrait. Une fois que nous avons l’arbre syntaxique abstrait, nous pouvons
facilement le parcourir et le modifier.
Alors que SableCC ne pouvait à l’origine générer que la sortie Java, nous avons maintenant
plusieurs options pour le langage de sortie. Malheureusement, les classes C # générées ne
sont pas partielles - une fonctionnalité qui aurait été très utile une fois que nous aurions
commencé à faire des choses plus avancées avec l’AST. Il est assez facile de modifier les
six fichiers sources manuellement, ainsi que de mettre en place un script automatisé pour le
faire pour nous.
Une fois que nous avons lu l’entrée et l’avons transformée en AST, nous avons utilisé une
approche basée sur la pile pour la parcourir et calculer les sous-résultats d’une manière très
simple, en émulant la façon dont la plupart des langages de niveau supérieur fonctionnent en
interne.
Mark S. Rasmussen
Je suis le CTO cheziPaperoù je me blottis contre les bases de données, le code
de moule et maintiens la responsabilité technique et d’équipe globale. Je suis un
conférencier passionné lors de groupes d’utilisateurs et de conférences. J’aime la
vie, les motos, la photographie et tout ce qui est technique. Dites bonjour
surTwitter, écrivez-moi une-mailou recherchez-moi surLinkedIn.
7 Comments
1 Login
LOG IN WITH
OR SIGN UP WITH DISQUS ?
Name
Sort by Oldest
Thanks for the links. The dynamic LINQ queries are interesting but
I don't think they allow for defining your own grammars and parsing
of the tree. That is, it may be hackable to do so but it's definitely no
parser generator :)
I didn't know of TinyPG, I'll definitely have a look at it. I'm hoping to
find the time for writing a comparison between different parsing
solutions like SableCC, TinyPG, Irony, Coco/R - preferably using
the same language grammar so a valid comparison can be made.
△ ▽ • Reply •
intellect.dk/...
△ ▽ • Reply •
Neat example!
get {
if (result == null)
return result.Value;
}
}
}
node.Apply(this);
Visit(node);
return result;
// Associative operators
lastResult = EvaluateExp(node.GetLeft()) *
EvaluateExp(node.GetRight());
}
...
△ ▽ • Reply •
get {
if (lastResult == null)
return lastResult.Value;
}
}
// Associative operators
lastResult = EvaluateExp(node.GetLeft()) *
EvaluateExp(node.GetRight());
// Calculate expression
calculator.Visit(ast);