Quels spécifiques avantages et inconvénients de chaque façon de travailler sur une grammaire de langage de programmation?
Pourquoi/quand devrais-je rouler le mien? Pourquoi/quand dois-je utiliser un générateur?
Il y a vraiment trois options, toutes trois préférables dans des situations différentes.
Dites, on vous demande de construire un analyseur pour un ancien format de données MAINTENANT. Ou vous avez besoin que votre analyseur soit rapide. Ou vous avez besoin que votre analyseur soit facile à entretenir.
Dans ces cas, il vaut probablement mieux utiliser un générateur d'analyseur. Vous n'avez pas besoin de jouer avec les détails, vous n'avez pas besoin d'obtenir beaucoup de code compliqué pour fonctionner correctement, vous écrivez simplement la grammaire à laquelle l'entrée adhère, écrivez du code de manipulation et presto: analyseur instantané.
Les avantages sont clairs:
Il y a une chose à laquelle vous devez faire attention avec les générateurs d'analyseurs: ils peuvent parfois rejeter vos grammaires. Pour un aperçu des différents types d'analyseurs et comment ils peuvent vous mordre, vous pouvez commencer ici . Ici vous pouvez trouver un aperçu de nombreuses implémentations et des types de grammaires qu'elles acceptent.
Les générateurs d'analyseurs sont agréables, mais ils ne sont pas très conviviaux (l'utilisateur final, pas vous). Vous ne pouvez généralement pas donner de bons messages d'erreur, ni fournir de récupération d'erreur. Peut-être que votre langue est très bizarre et que les analyseurs rejettent votre grammaire ou que vous avez besoin de plus de contrôle que le générateur ne vous en donne.
Dans ces cas, l'utilisation d'un analyseur de descente récursive manuscrite est probablement la meilleure. Bien que cela soit compliqué, vous avez un contrôle total sur votre analyseur afin que vous puissiez faire toutes sortes de choses sympas que vous ne pouvez pas faire avec des générateurs d'analyseurs, comme des messages d'erreur et même la récupération d'erreurs (essayez de supprimer tous les points-virgules d'un fichier C # : le compilateur C # se plaindra, mais détectera de toute façon la plupart des autres erreurs indépendamment de la présence de points-virgules).
Les analyseurs manuscrits fonctionnent également mieux que ceux générés, en supposant que la qualité de l'analyseur est suffisamment élevée. D'un autre côté, si vous n'arrivez pas à écrire un bon analyseur - généralement en raison (d'une combinaison de) manque d'expérience, de connaissances ou de conception - alors les performances sont généralement plus lentes. Pour les lexers, l'inverse est cependant vrai: les lexers générés généralement utilisent des recherches de table, ce qui les rend plus rapides que (la plupart) celles écrites à la main.
Du point de vue de l'éducation, l'écriture de votre propre analyseur vous apprendra plus que l'utilisation d'un générateur. Vous devez écrire du code de plus en plus compliqué après tout, et vous devez comprendre exactement comment vous analysez un langage. D'un autre côté, si vous voulez apprendre à créer votre propre langue (donc, acquérir de l'expérience dans la conception de langues), l'option 1 ou l'option 3 est préférable: si vous développez une langue, cela changera probablement beaucoup, et les options 1 et 3 vous facilitent la tâche.
C'est le chemin que je suis en train de parcourir: vous écrivez le vôtre générateur d'analyseur. Bien que très simple, cela vous apprendra probablement le plus.
Pour vous donner une idée de ce qu'implique un projet comme celui-ci, je vais vous parler de mes propres progrès.
Le générateur de lexer
J'ai d'abord créé mon propre générateur de lexer. Je conçois généralement des logiciels en commençant par la façon dont le code sera utilisé, j'ai donc pensé à comment je voulais pouvoir utiliser mon code et j'ai écrit ce morceau de code (c'est en C #):
Lexer<CalculatorToken> calculatorLexer = new Lexer<CalculatorToken>(
new List<StringTokenPair>()
{ // This is just like a Lex specification:
// regex token
new StringTokenPair("\\+", CalculatorToken.Plus),
new StringTokenPair("\\*", CalculatorToken.Times),
new StringTokenPair("(", CalculatorToken.LeftParenthesis),
new StringTokenPair(")", CalculatorToken.RightParenthesis),
new StringTokenPair("\\d+", CalculatorToken.Number),
});
foreach (CalculatorToken token in
calculatorLexer.GetLexer(new StringReader("15+4*10")))
{ // This will iterate over all tokens in the string.
Console.WriteLine(token.Value);
}
// Prints:
// 15
// +
// 4
// *
// 10
Les paires chaîne-jeton d'entrée sont converties en une structure récursive correspondante décrivant les expressions régulières qu'elles représentent en utilisant les idées d'une pile arithmétique. Celui-ci est ensuite converti en NFA (automate fini non déterministe), qui est à son tour converti en DFA (automate fini déterministe). Vous pouvez ensuite faire correspondre les chaînes avec le DFA.
De cette façon, vous avez une bonne idée du fonctionnement exact des lexers. De plus, si vous le faites correctement, les résultats de votre générateur de lexer peuvent être à peu près aussi rapides que les implémentations professionnelles. Vous ne perdez également aucune expressivité par rapport à l'option 2, et pas beaucoup d'expressivité par rapport à l'option 1.
J'ai implémenté mon générateur de lexer dans un peu plus de 1600 lignes de code. Ce code fait fonctionner ce qui précède, mais il génère toujours le lexer à la volée à chaque démarrage du programme: je vais ajouter du code pour l'écrire sur le disque à un moment donné.
Si vous voulez savoir comment écrire votre propre lexer, this est un bon point de départ.
Le générateur d'analyseur
Vous écrivez ensuite votre générateur d'analyseur. Je me réfère à ici pour un aperçu des différents types d'analyseurs - en règle générale, plus ils peuvent analyser, plus ils sont lents.
La vitesse n'étant pas un problème pour moi, j'ai choisi d'implémenter un analyseur Earley. Les implémentations avancées d'un analyseur Earley ont été montrées pour être environ deux fois plus lent que les autres types d'analyseur.
En échange de ce coup de vitesse, vous avez la possibilité d'analyser n'importe quoi type de grammaire, même ambiguë. Cela signifie que vous n'avez jamais à vous soucier de savoir si votre analyseur contient une récursion à gauche ou de ce qu'est un conflit de réduction de décalage. Vous pouvez également définir des grammaires plus facilement à l'aide de grammaires ambiguës, peu importe l'arbre d'analyse qui en résulte, par exemple peu importe que vous analysiez 1 + 2 + 3 comme (1 + 2) +3 ou comme 1 + (2 + 3).
Voici à quoi peut ressembler un morceau de code utilisant mon générateur d'analyseur:
Lexer<CalculatorToken> calculatorLexer = new Lexer<CalculatorToken>(
new List<StringTokenPair>()
{
new StringTokenPair("\\+", CalculatorToken.Plus),
new StringTokenPair("\\*", CalculatorToken.Times),
new StringTokenPair("(", CalculatorToken.LeftParenthesis),
new StringTokenPair(")", CalculatorToken.RightParenthesis),
new StringTokenPair("\\d+", CalculatorToken.Number),
});
Grammar<IntWrapper, CalculatorToken> calculator
= new Grammar<IntWrapper, CalculatorToken>(calculatorLexer);
// Declaring the nonterminals.
INonTerminal<IntWrapper> expr = calculator.AddNonTerminal<IntWrapper>();
INonTerminal<IntWrapper> term = calculator.AddNonTerminal<IntWrapper>();
INonTerminal<IntWrapper> factor = calculator.AddNonTerminal<IntWrapper>();
// expr will be our head nonterminal.
calculator.SetAsMainNonTerminal(expr);
// expr: term | expr Plus term;
calculator.AddProduction(expr, term.GetDefault());
calculator.AddProduction(expr,
expr.GetDefault(),
CalculatorToken.Plus.GetDefault(),
term.AddCode(
(x, r) => { x.Result.Value += r.Value; return x; }
));
// term: factor | term Times factor;
calculator.AddProduction(term, factor.GetDefault());
calculator.AddProduction(term,
term.GetDefault(),
CalculatorToken.Times.GetDefault(),
factor.AddCode
(
(x, r) => { x.Result.Value *= r.Value; return x; }
));
// factor: LeftParenthesis expr RightParenthesis
// | Number;
calculator.AddProduction(factor,
CalculatorToken.LeftParenthesis.GetDefault(),
expr.GetDefault(),
CalculatorToken.RightParenthesis.GetDefault());
calculator.AddProduction(factor,
CalculatorToken.Number.AddCode
(
(x, s) => { x.Result = new IntWrapper(int.Parse(s));
return x; }
));
IntWrapper result = calculator.Parse("15+4*10");
// result == 55
(Notez que IntWrapper est simplement un Int32, sauf que C # l'exige comme classe, donc j'ai dû introduire une classe wrapper)
J'espère que vous voyez que le code ci-dessus est très puissant: toute grammaire que vous pouvez trouver peut être analysée. Vous pouvez ajouter des bits de code arbitraires dans la grammaire capables d'effectuer de nombreuses tâches. Si vous parvenez à faire fonctionner tout cela, vous pouvez réutiliser le code résultant pour effectuer beaucoup de tâches très facilement: imaginez simplement construire un interpréteur de ligne de commande en utilisant ce morceau de code.
Si vous n'avez jamais, jamais écrit un analyseur, je vous recommande de le faire. C'est amusant, et vous apprenez comment les choses fonctionnent, et vous apprenez à apprécier l'effort que les générateurs d'analyseurs et de lexers vous évitent de faire la prochaine fois dont vous avez besoin un analyseur.
Je vous suggère également d'essayer de lire http://compilers.iecc.com/crenshaw/ car il a une attitude très terre-à-terre quant à la façon de le faire.
L'avantage d'écrire votre propre analyseur de descente récursive est que vous pouvez générer messages d'erreur de haute qualité sur les erreurs de syntaxe. À l'aide de générateurs d'analyseurs, vous pouvez créer des erreurs de production et ajouter des messages d'erreur personnalisés à certains moments, mais les générateurs d'analyseurs ne correspondent tout simplement pas à la puissance d'avoir un contrôle complet sur l'analyse.
Un autre avantage de l'écriture de la vôtre est qu'il est plus facile d'analyser une représentation plus simple qui n'a pas de correspondance un à un avec votre grammaire.
Si votre grammaire est corrigée et que les messages d'erreur sont importants, envisagez de lancer la vôtre, ou au moins d'utiliser un générateur d'analyseur qui vous donne les messages d'erreur dont vous avez besoin. Si votre grammaire change constamment, vous devriez plutôt envisager d'utiliser des générateurs d'analyseurs.
Bjarne Stroustrup explique comment il a utilisé YACC pour la première implémentation de C++ (voir La conception et l'évolution de C++ ). Dans ce premier cas, il aurait préféré écrire son propre analyseur récursif de descente à la place!
Option 3: Ni l'un ni l'autre (Lancez votre propre générateur d'analyseur)
Juste parce qu'il y a une raison pour ne pas utiliser ANTLR , bison , Coco/R , Grammatica , JavaCC , citron , étuvé , SableCC , Quex , etc - cela ne signifie pas que vous devez immédiatement lancer votre propre analyseur + lexer.
Identifiez pourquoi tous ces outils ne sont pas assez bons - pourquoi ne vous permettent-ils pas d'atteindre votre objectif?
À moins que vous ne soyez certain que les bizarreries de la grammaire que vous traitez sont uniques, vous ne devez pas simplement créer un analyseur + lexer personnalisé pour cela. Au lieu de cela, créez un outil qui créera ce que vous voulez, mais peut également être utilisé pour répondre aux besoins futurs, puis publiez-le en tant que logiciel libre pour éviter que d'autres personnes aient le même problème que vous.
Rouler votre propre analyseur vous oblige à penser directement à la complexité de votre langue. Si la langue est difficile à analyser, elle sera probablement difficile à comprendre.
Il y avait beaucoup d'intérêt pour les générateurs d'analyseurs dans les premiers jours, motivés par une syntaxe de langage très compliquée (certains diraient "torturée"). JOVIAL était un exemple particulièrement mauvais: il fallait une anticipation de deux symboles, à une époque où tout le reste ne demandait au plus qu'un symbole. Cela a rendu la génération de l'analyseur pour un compilateur JOVIAL plus difficile que prévu (car General Dynamics/Fort Worth Division a appris à la dure quand ils ont acheté des compilateurs JOVIAL pour le programme F-16).
Aujourd'hui, la descente récursive est universellement la méthode préférée, car elle est plus facile pour les rédacteurs de compilateurs. Les compilateurs de descente récursive récompensent fortement la conception d'un langage simple et propre, en ce sens qu'il est beaucoup plus facile d'écrire un analyseur de descente récursive pour un langage simple et propre que pour un langage alambiqué et désordonné.
Enfin: Avez-vous envisagé d'intégrer votre langue dans LISP et de laisser un interprète LISP faire le gros du travail pour vous? AutoCAD l'a fait et a trouvé que cela leur facilitait la vie. Il existe de nombreux interprètes LISP légers, dont certains peuvent être intégrés.
J'ai écrit une fois un analyseur pour une application commerciale et j'ai utilisé yacc. Il y avait un prototype concurrent où un développeur a écrit le tout à la main en C++ et cela a fonctionné environ cinq fois plus lentement.
Quant au lexer de cet analyseur, je l'ai écrit entièrement à la main. Il a fallu - désolé, c'était il y a presque 10 ans, donc je ne m'en souviens pas précisément - environ 1000 lignes en C .
La raison pour laquelle j'ai écrit le lexer à la main était la grammaire d'entrée de l'analyseur. C'était une exigence, quelque chose que ma mise en œuvre de l'analyseur devait respecter, par opposition à quelque chose que j'avais conçu. (Bien sûr, je l'aurais conçu différemment. Et mieux!) La grammaire était très dépendante du contexte et même le lexisme dépendait de la sémantique à certains endroits. Par exemple, un point-virgule peut faire partie d'un jeton à un endroit, mais un séparateur à un endroit différent - basé sur une interprétation sémantique d'un élément qui a été analysé auparavant. Donc, j'ai "enterré" ces dépendances sémantiques dans le lexer manuscrit et cela m'a laissé un assez simple [~ # ~] bnf [~ # ~] qui était facile à implémenter dans yacc.
AJOUTÉ en réponse à Macneil: yacc fournit une abstraction très puissante qui permet au programmeur de penser en termes de terminaux, non-terminaux, des productions et des trucs comme ça. De plus, lors de l'implémentation de la fonction yylex()
, cela m'a aidé à me concentrer sur le retour du jeton actuel et à ne pas me soucier de ce qui était avant ou après. Le programmeur C++ a travaillé au niveau des caractères, sans l'avantage d'une telle abstraction et a fini par créer un algorithme plus compliqué et moins efficace. Nous avons conclu que la vitesse plus lente n'avait rien à voir avec le C++ lui-même ou les bibliothèques. Nous avons mesuré la vitesse d'analyse pure avec des fichiers chargés en mémoire; si nous avions un problème de mise en mémoire tampon des fichiers, yacc ne serait pas notre outil de choix pour le résoudre.
VOULEZ ÉGALEMENT AJOUTER: ce n'est pas une recette pour écrire des analyseurs en général, juste un exemple de la façon dont cela a fonctionné dans une situation particulière.
Cela dépend de votre objectif.
Essayez-vous d'apprendre comment fonctionnent les analyseurs/compilateurs? Ensuite, écrivez le vôtre à partir de zéro. C'est la seule façon d'apprendre vraiment à apprécier tous les tenants et aboutissants de ce qu'ils font. J'en ai écrit un au cours des derniers mois, et ça a été une expérience intéressante et précieuse, en particulier les moments "ah, c'est pourquoi le langage X fait ça ...".
Avez-vous besoin de préparer rapidement quelque chose pour une demande dans un délai? Ensuite, utilisez peut-être un outil d'analyse.
Avez-vous besoin de quelque chose que vous voudrez développer au cours des 10, 20, voire 30 prochaines années? Écrivez le vôtre et prenez votre temps. Ça vaudra bien la peine.
Cela dépend entièrement de ce que vous devez analyser. Pouvez-vous rouler le vôtre plus rapidement que vous ne pourriez atteindre la courbe d'apprentissage d'un lexer? Les éléments à analyser sont-ils suffisamment statiques pour que vous ne regrettiez pas la décision plus tard? Trouvez-vous les implémentations existantes trop complexes? Si c'est le cas, amusez-vous à rouler le vôtre, mais seulement si vous ne contournez pas une courbe d'apprentissage.
Dernièrement, j'ai vraiment aimé le analyseur de citron , qui est sans doute le plus simple et le plus facile que j'aie jamais utilisé. Dans un souci de facilité d'entretien, je l'utilise pour la plupart des besoins. SQLite l'utilise ainsi que certains autres projets notables.
Mais, je ne suis pas du tout intéressé par les lexers, au-delà d'eux ne me gênant pas quand j'en ai besoin (donc du citron). Vous pourriez l'être, et si oui, pourquoi ne pas en faire un? J'ai le sentiment que vous reviendrez à utiliser celui qui existe, mais grattez-le si vous devez :)
Avez-vous envisagé approche de l'établi linguistique Martin Fowlers ? Citation de l'article
Le changement le plus évident qu'un pupitre de langue apporte à l'équation est la facilité de création de DSL externes. Vous n'avez plus besoin d'écrire un analyseur. Vous devez définir la syntaxe abstraite - mais c'est en fait une étape de modélisation des données assez simple. De plus, votre DSL obtient un puissant IDE - bien que vous deviez passer un peu de temps à définir cet éditeur. Le générateur est toujours quelque chose que vous devez faire, et mon sentiment est que ce n'est pas beaucoup Mais la construction d'un générateur pour une DSL simple et efficace est l'une des parties les plus faciles de l'exercice.
En lisant cela, je dirais que l'époque de l'écriture de votre propre analyseur est révolue et qu'il est préférable d'utiliser l'une des bibliothèques disponibles. Une fois que vous maîtrisez la bibliothèque, tous les DSL que vous créerez à l'avenir bénéficieront de cette connaissance. De plus, d'autres n'ont pas à apprendre votre approche de l'analyse.
Modifier pour couvrir le commentaire (et la question révisée)
Avantages de rouler le vôtre
Donc, en bref, vous devez rouler le vôtre lorsque vous voulez vraiment pirater les entrailles d'un problème sérieusement difficile que vous vous sentez fortement motivé à maîtriser.
Avantages d'utiliser la bibliothèque de quelqu'un d'autre
Par conséquent, si vous voulez un résultat final rapide, utilisez la bibliothèque de quelqu'un d'autre.
Dans l'ensemble, cela se résume à un choix de combien vous voulez posséder le problème, et donc la solution. Si vous voulez tout, lancez le vôtre.
Le grand avantage d'écrire le vôtre est que vous saurez écrire le vôtre. Le gros avantage d'utiliser un outil comme yacc est que vous saurez comment utiliser l'outil. Je suis fan de cime des arbres pour l'exploration initiale.
Pourquoi ne pas créer un générateur d'analyseur open-source et le personnaliser? Si vous n'utilisez pas de générateurs d'analyseurs, votre code sera très difficile à maintenir, si vous avez fait de gros changements dans la syntaxe de votre langue.
Dans mes analyseurs, j'ai utilisé des expressions régulières (je veux dire, style Perl) pour symboliser et utiliser certaines fonctions pratiques pour augmenter la lisibilité du code. Cependant, un code généré par l'analyseur peut être plus rapide en créant des tables d'état et de longs switch
-case
s, ce qui peut augmenter la taille du code source à moins que vous .gitignore
leur.
Voici deux exemples de mes analyseurs personnalisés:
https://github.com/SHiNKiROU/DesignScript - un dialecte BASIC, parce que j'étais trop paresseux pour écrire des lookaheads en notation tableau, j'ai sacrifié la qualité des messages d'erreur https: // github. com/SHiNKiROU/ExprParser - Un calculateur de formule. Remarquez les astuces de métaprogrammation étranges
"Dois-je utiliser cette" roue "éprouvée ou la réinventer?"