Quelles sont les bonnes ressources pour écrire un lexer en C ++ (livres, tutoriels, documents), quelles sont les bonnes techniques et pratiques?
J'ai regardé sur Internet et tout le monde dit d'utiliser un générateur de lexer comme lex. Je ne veux pas faire ça, je veux écrire un lexer à la main.
Réponses:
Gardez à l'esprit que chaque machine à états finis correspond à une expression régulière, ce qui correspond à un programme structuré utilisant des instructions
if
etwhile
.Ainsi, par exemple, pour reconnaître des entiers, vous pouvez avoir la machine d'état:
ou l'expression régulière:
ou le code structuré:
Personnellement, j'écris toujours des lexers en utilisant ce dernier, car à mon humble avis ce n'est pas moins clair, et il n'y a rien de plus rapide.
la source
*pc
, non? Commewhile(isdigit(*pc)) { value += pc; pc++; }
. Ensuite,}
vous convertissez la valeur en nombre et l'assignez à un jeton.n = n * 10 + (*pc++ - '0');
. Il devient un peu plus complexe pour la notation à virgule flottante et «e», mais pas mal. Je suis sûr que je pourrais enregistrer un peu de code en emballant les caractères dans un tampon et en appelantatof
ou autre chose. Cela ne fonctionnerait pas plus vite.Les Lexers sont des machines à états finis. Par conséquent, ils peuvent être construits par n'importe quelle bibliothèque FSM à usage général. Pour les besoins de ma propre éducation, cependant, j'ai écrit la mienne, en utilisant des modèles d'expression. Voici mon lexer:
Il est soutenu par une bibliothèque de machines à états finis, à suivi arrière, basée sur un itérateur, d'une longueur d'environ 400 lignes. Cependant, il est facile de voir que tout ce que j'avais à faire était de construire de simples opérations booléennes, comme
and
,or
etnot
, et quelques opérateurs de style regex comme*
pour zéro ou plus,eps
pour signifier "correspondre à n'importe quoi" etopt
signifier "correspondre rien mais ne le consomme pas ". La bibliothèque est entièrement générique et basée sur des itérateurs. Le truc MakeEquality est un simple test d'égalité entre*it
et la valeur transmise, et MakeRange est un<= >=
test simple .Finalement, je prévois de passer du retour en arrière à la prévision.
la source
MakeEquality
extrait? Plus précisément, l'objet renvoyé par cette fonction. Semble très intéressant.Tout d'abord, il se passe différentes choses ici:
En général, nous nous attendons à ce qu'un lexer effectue les 3 étapes en une seule fois, mais cette dernière est intrinsèquement plus difficile et il y a quelques problèmes avec l'automatisation (plus à ce sujet plus tard).
Le lexer le plus étonnant que je connaisse est Boost.Spirit.Qi . Il utilise des modèles d'expression pour générer vos expressions lexères, et une fois habitué à sa syntaxe, le code se sent vraiment bien. Cependant, il se compile très lentement (modèles lourds), il est donc préférable d'isoler les différentes parties dans des fichiers dédiés pour éviter de les recompiler quand ils n'ont pas été touchés.
Il y a quelques pièges dans les performances, et l'auteur du compilateur Epoch explique comment il a obtenu une accélération de 1000x par un profilage et une enquête intensifs sur le fonctionnement de Qi dans un article .
Enfin, il existe également du code généré par des outils externes (Yacc, Bison, ...).
Mais j'ai promis un article sur ce qui n'allait pas avec l'automatisation de la vérification de la grammaire.
Si vous consultez Clang, par exemple, vous vous rendrez compte qu'au lieu d'utiliser un analyseur généré et quelque chose comme Boost.Spirit, ils ont plutôt décidé de valider la grammaire manuellement en utilisant une technique générique d'analyse de descente. Cela semble sûrement en arrière?
En fait, il y a une raison très simple: la récupération d'erreur .
L'exemple typique, en C ++:
Remarquez l'erreur? Un point-virgule manquant juste après la déclaration de
Foo
.C'est une erreur courante, et Clang récupère parfaitement en réalisant qu'elle est simplement manquante et
void
n'est pas une instanceFoo
mais une partie de la prochaine déclaration. Cela évite de diagnostiquer les messages d'erreur cryptiques.La plupart des outils automatisés n'ont aucun moyen (au moins évident) de spécifier ces erreurs probables et comment s'en remettre. Souvent, la récupération nécessite une petite analyse syntaxique, donc c'est loin d'être évident.
Il y a donc un compromis à faire avec l'utilisation d'un outil automatisé: vous obtenez votre analyseur rapidement, mais il est moins convivial.
la source
Puisque vous voulez savoir comment fonctionnent les lexers, je suppose que vous voulez réellement savoir comment fonctionnent les générateurs de lexers.
Un générateur de lexer prend une spécification lexicale, qui est une liste de règles (paires d'expression régulière-jeton), et génère un lexer. Ce lexer résultant peut ensuite transformer une chaîne d'entrée (caractère) en une chaîne de jeton selon cette liste de règles.
La méthode la plus couramment utilisée consiste principalement à transformer une expression régulière en automates finis déterministes (DFA) via un automate non déterministe (NFA), plus quelques détails.
Un guide détaillé de cette transformation peut être trouvé ici . Notez que je ne l'ai pas lu moi-même, mais il semble assez bon. De plus, à peu près n'importe quel livre sur la construction d'un compilateur présentera cette transformation dans les premiers chapitres.
Si vous êtes intéressé par des diapositives de cours sur le sujet, il y en a sans aucun doute une quantité infinie parmi les cours sur la construction de compilateurs. De mon université, vous pouvez trouver de telles diapositives ici et ici .
Il y a peu d'autres choses qui ne sont pas couramment utilisées dans les lexers ou traitées dans les textes, mais qui sont néanmoins très utiles:
Premièrement, la gestion d'Unicode est quelque peu banale. Le problème est que l'entrée ASCII ne fait que 8 bits de large, ce qui signifie que vous pouvez facilement avoir une table de transition pour chaque état dans le DFA, car ils n'ont que 256 entrées. Cependant, Unicode, ayant une largeur de 16 bits (si vous utilisez UTF-16), nécessite 64k tables pour chaque entrée dans le DFA. Si vous avez des grammaires complexes, cela peut commencer à prendre beaucoup de place. Remplir ces tableaux commence également à prendre un peu de temps.
Alternativement, vous pouvez générer des arbres d'intervalles. Un arbre de plage peut contenir les tuples ('a', 'z'), ('A', 'Z') par exemple, ce qui est beaucoup plus efficace en mémoire que d'avoir la table complète. Si vous conservez des intervalles sans chevauchement, vous pouvez utiliser n'importe quel arbre binaire équilibré à cet effet. Le temps d'exécution est linéaire dans le nombre de bits dont vous avez besoin pour chaque caractère, donc O (16) dans le cas Unicode. Cependant, dans le meilleur des cas, ce sera généralement un peu moins.
Un autre problème est que les lexers générés couramment ont en fait une performance quadratique dans le pire des cas. Bien que ce comportement du pire des cas ne soit pas communément observé, il pourrait vous mordre. Si vous rencontrez le problème et souhaitez le résoudre, un article décrivant comment obtenir un temps linéaire peut être trouvé ici .
Vous voudrez probablement pouvoir décrire les expressions régulières sous forme de chaînes, telles qu'elles apparaissent normalement. Cependant, l'analyse de ces descriptions d'expressions régulières dans des NFA (ou éventuellement une structure intermédiaire récursive en premier) est un peu un problème d'oeuf de poule. Pour analyser les descriptions d'expressions régulières, l'algorithme Shunting Yard est très approprié. Wikipédia semble avoir une longue page sur l'algorithme .
la source