Chaque fois que j'écris un lexer et un analyseur simple, je tombe sur la même question : comment le lexer et l'analyseur doivent-ils communiquer ? Je vois quatre approches différentes :
-
Le lexeur convertit rapidement la chaîne d'entrée entière en un vecteur de mots. Une fois cette opération effectuée, le vecteur est transmis à l'analyseur syntaxique qui le convertit en un arbre. C'est de loin la solution la plus simple à mettre en œuvre, mais comme tous les tokens sont stockés en mémoire, elle gaspille beaucoup d'espace.
-
Chaque fois que le lexeur trouve un token, il invoque une fonction sur l'analyseur syntaxique, en passant le token courant. D'après mon expérience, cela ne fonctionne que si l'analyseur syntaxique peut naturellement être implémenté comme une machine à état comme les analyseurs syntaxiques LALR. En revanche, je ne pense pas que cela fonctionne du tout pour les analyseurs descendants récursifs.
-
Chaque fois que l'analyseur a besoin d'un token, il demande au lexer le token suivant. Ceci est très facile à mettre en œuvre en C# grâce à la fonction
yield
mais assez difficile en C++ qui ne le possède pas. -
Le lexeur et l'analyseur syntaxique communiquent par le biais d'une file d'attente asynchrone. Cette solution est connue sous le nom de "producteur/consommateur" et devrait simplifier considérablement la communication entre le lexer et l'analyseur syntaxique. Est-ce que cette solution est également plus performante que les autres sur les multicoeurs ? Ou le lexing est-il trop trivial ?
Mon analyse est-elle valable ? Existe-t-il d'autres approches auxquelles je n'ai pas pensé ? Qu'est-ce qui est utilisé dans les compilateurs du monde réel ? Ce serait vraiment cool si des auteurs de compilateurs comme Eric Lippert pouvaient apporter un éclairage sur cette question.