web-dev-qa-db-fra.com

Existe-t-il une alternative pour flex / bison utilisable sur les systèmes embarqués 8 bits?

J'écris un petit interpréteur pour un langage simple comme BASIC comme un exercice sur un microcontrôleur AVR en C en utilisant la chaîne d'outils avr-gcc. Cependant, je me demande s'il existe des outils open source qui pourraient m'aider à écrire le lexer et l'analyseur.

Si j'écrivais ceci pour fonctionner sur ma machine Linux, je pourrais utiliser flex/bison. Maintenant que je me suis limité à une plate-forme 8 bits, je dois tout faire à la main ou non?

78
Johan

J'ai implémenté un analyseur pour un langage de commande simple ciblé pour ATmega328p . Cette puce a 32k ROM et seulement 2k RAM. La RAM est certainement la limitation la plus importante - si vous n'êtes pas encore lié à une puce particulière, choisissez-en un avec autant RAM que possible. Cela vous facilitera la vie.

Au début, j'ai envisagé d'utiliser flex/bison. J'ai décidé contre cette option pour deux raisons principales:

  • Par défaut, Flex & Bison dépendent de certaines fonctions de bibliothèque standard (en particulier pour les E/S) qui ne sont pas disponibles ou ne fonctionnent pas de la même manière dans avr-libc. Je suis sûr que des solutions de contournement sont prises en charge, mais c'est un effort supplémentaire que vous devrez prendre en compte.
  • AVR a un Harvard Architecture . C n'est pas conçu pour tenir compte de cela, donc même les variables constantes sont chargées dans RAM par défaut . Vous avez utiliser des macros/fonctions spéciales pour stocker et accéder aux données dans flash et EEPROM . Flex & Bison en crée quelques-uns relativement de grandes tables de recherche, et celles-ci mangeront votre RAM assez rapidement. À moins que je ne me trompe (ce qui est tout à fait possible), vous devrez modifier la source de sortie afin de profitez des interfaces spéciales Flash et EEPROM.

Après avoir rejeté Flex & Bison, je suis allé chercher d'autres outils de générateur. En voici quelques-unes que j'ai envisagées:

Vous voudrez peut-être aussi jeter un œil à comparaison de Wikipedia .

En fin de compte, j'ai fini par coder à la main le lexer et l'analyseur.

Pour l'analyse, j'ai utilisé un analyseur de descente récursif. Je pense que Ira Baxter a déjà fait un travail adéquat pour couvrir ce sujet, et il y a beaucoup de tutoriels en ligne.

Pour mon lexer, j'ai écrit des expressions régulières pour tous mes terminaux, schématisé la machine à états équivalente et l'implémentée comme une fonction géante en utilisant les goto pour sauter entre les états. C'était fastidieux, mais les résultats ont très bien fonctionné. En passant, goto est un excellent outil pour implémenter des machines à états - tous vos états peuvent avoir des étiquettes claires juste à côté du code pertinent, il n'y a pas d'appel de fonction ou de surcharge de variable d'état, et c'est à peu près aussi rapide que vous pouvez obtenir. C n'a vraiment pas de meilleure construction pour construire des machines à états statiques.

Quelque chose à penser: les lexers ne sont vraiment qu'une spécialisation des analyseurs. La plus grande différence est que les grammaires régulières sont généralement suffisantes pour l'analyse lexicale, alors que la plupart des langages de programmation ont (principalement) des grammaires sans contexte. Il n'y a donc rien qui vous empêche d'implémenter un lexer comme analyseur de descente récursif ou d'utiliser un générateur d'analyseur pour écrire un lexer. Ce n'est généralement pas aussi pratique que d'utiliser un outil plus spécialisé.

54
Steve S

Si vous voulez un moyen facile de coder les analyseurs, ou si vous manquez d'espace, vous devez coder manuellement un analyseur de descente récursif; ce sont essentiellement LL (1) analyseurs. Ceci est particulièrement efficace pour les langues aussi "simples" que Basic. (J'en ai fait plusieurs dans les années 70!). La bonne nouvelle est qu'elles ne contiennent aucun code de bibliothèque; juste ce que vous écrivez.

Ils sont assez faciles à coder, si vous avez déjà une grammaire. Tout d'abord, vous devez vous débarrasser des règles récursives de gauche (par exemple, X = X Y). C'est généralement assez facile à faire, donc je le laisse comme un exercice. (Vous n'avez pas à le faire pour les règles de formation de liste; voir la discussion ci-dessous).

Ensuite, si vous avez la règle BNF du formulaire:

 X = A B C ;

créer un sous-programme pour chaque élément de la règle (X, A, B, C) qui renvoie un booléen disant "J'ai vu la construction de syntaxe correspondante". Pour X, codez:

subroutine X()
     if ~(A()) return false;
     if ~(B()) { error(); return false; }
     if ~(C()) { error(); return false; }
     // insert semantic action here: generate code, do the work, ....
     return true;
end X;

De même pour A, B, C.

Si un jeton est un terminal, écrivez du code qui vérifie le flux d'entrée pour la chaîne de caractères qui compose le terminal. Par exemple, pour un nombre, vérifiez que le flux d'entrée contient des chiffres et avancez le curseur du flux d'entrée au-delà des chiffres. Ceci est particulièrement facile si vous analysez un tampon (pour BASIC, vous avez tendance à obtenir une ligne à la fois) en avançant simplement ou non un pointeur de balayage de tampon. Ce code est essentiellement la partie lexère de l'analyseur.

Si votre règle BNF est récursive ... ne vous inquiétez pas. Il suffit de coder l'appel récursif. Cela gère les règles de grammaire comme:

T  =  '('  T  ')' ;

Cela peut être codé comme suit:

subroutine T()
     if ~(left_paren()) return false;
     if ~(T()) { error(); return false; }
     if ~(right_paren()) { error(); return false; }
     // insert semantic action here: generate code, do the work, ....
     return true;
end T;

Si vous avez une règle BNF avec une alternative:

 P = Q | R ;

puis codez P avec des choix alternatifs:

subroutine P()
    if ~(Q())
        {if ~(R()) return false;
         return true;
        }
    return true;
end P;

Parfois, vous rencontrerez des règles de formation de liste. Ceux-ci ont tendance à être récursifs et ce cas est facilement géré. L'idée de base est d'utiliser l'itération plutôt que la récursivité, et cela évite la récursion infinie que vous obtiendriez en faisant la manière "évidente". Exemple:

L  =  A |  L A ;

Vous pouvez coder cela en utilisant l'itération comme:

subroutine L()
    if ~(A()) then return false;
    while (A()) do { /* loop */ }
    return true;
end L;

Vous pouvez coder plusieurs centaines de règles de grammaire en un jour ou deux de cette façon. Il y a plus de détails à remplir, mais les bases ici devraient être plus que suffisantes.

Si vous êtes vraiment restreint en espace, vous pouvez construire une machine virtuelle qui implémente ces idées. C'est ce que j'ai fait dans les années 70, quand on pouvait obtenir des mots 8K 16 bits.


Si vous ne voulez pas coder cela à la main, vous pouvez l'automatiser avec un métacompilateur ( Meta II ) qui produit essentiellement la même chose. Ce sont des divertissements techniques époustouflants qui prennent vraiment tout le travail à faire, même pour les grandes grammaires.

Août 2014:

Je reçois beaucoup de demandes pour "comment construire un AST avec un analyseur". Pour plus de détails à ce sujet, qui élabore essentiellement cette réponse, voir mes autres SO = réponse https://stackoverflow.com/a/25106688/12016

Juillet 2015:

Il y a beaucoup de gens qui veulent écrire un évaluateur d'expression simple. Vous pouvez le faire en faisant le même genre de choses que le lien "AST builder" ci-dessus suggère; faites simplement de l'arithmétique au lieu de construire des nœuds d'arbre. Voici n évaluateur d'expression fait de cette façon .

195
Ira Baxter

Vous pouvez utiliser flex/bison sous Linux avec son gcc natif pour générer le code que vous compilerez ensuite avec votre gcc AVR pour la cible intégrée.

11
Paul R

GCC peut effectuer une compilation croisée sur diverses plates-formes, mais vous exécutez flex et bison sur la plate-forme sur laquelle vous exécutez le compilateur. Ils crachent juste du code C que le compilateur construit ensuite. Testez-le pour voir la taille réelle de l'exécutable résultant. Notez qu'ils ont des bibliothèques d'exécution (libfl.a etc.) que vous devrez également croiser pour compiler vers votre cible.

2