C'est un sujet important, mais plutôt que de vous effacer par un pompeux "allez lire un livre, gamin", je me ferai un plaisir de vous donner des conseils pour vous aider à bien comprendre.
La plupart des compilateurs et / ou interprètes fonctionnent comme ceci:
Tokenize : Scannez le texte du code et divisez -le en une liste de jetons.
Cette étape peut être délicate car vous ne pouvez pas simplement diviser la chaîne d'espaces, vous devez reconnaître qu'il if (bar) foo += "a string";
s'agit d'une liste de 8 jetons: WORD, OPEN_PAREN, WORD, CLOSE_PAREN, WORD, ASIGNMENT_ADD, STRING_LITERAL, TERMINATOR. Comme vous pouvez le constater, diviser simplement le code source en espaces ne fonctionnera pas. Vous devez lire chaque caractère sous forme de séquence. Ainsi, si vous rencontrez un caractère alphanumérique, continuez à lire les caractères jusqu'à ce que vous trouviez un caractère non alphanum et cette chaîne. Il suffit de lire est un mot à classer plus tard. Vous pouvez décider vous-même de la granularité de votre tokenizer: soit-il avalera-t-il "a string"
sous la forme d'un jeton appelé STRING_LITERAL pour être analysé plus tard, ou s'il verra"a string"
comme OPEN_QUOTE, UNPARSED_TEXT, CLOSE_QUOTE ou autre, il ne s'agit que de l'un des nombreux choix que vous devez choisir vous-même au moment de le coder.
Lex : Alors maintenant vous avez une liste de jetons. Vous avez probablement étiqueté des jetons avec une classification ambiguë, telle que WORD, car lors du premier passage, vous ne passez pas trop de temps à essayer de comprendre le contexte de chaque chaîne de caractères. Alors maintenant, relisez votre liste de jetons sources et reclassifiez chacun des jetons ambigus avec un type de jeton plus spécifique en fonction des mots-clés de votre langue. Donc, vous avez un mot tel que "si", et "si" est dans votre liste de mots-clés spéciaux appelés symbole SI afin de changer le type de symbole de ce jeton de mot à mot, et tout mot qui ne figure pas dans votre liste de mots-clés spéciaux , comme WORD foo, est un IDENTIFIANT.
Parse : Alors maintenant, vous avez tourné if (bar) foo += "a string";
une liste de jetons lexés qui ressemble à ceci: IF IDENTIFIANT OPEN_PAREN IDENTIFIANT CLOSE_PAREN ASIGN_ADD STRING_LITERAL TERMINATOR. L'étape consiste à reconnaître les séquences de jetons en tant qu'énoncés. Ceci est l'analyse. Vous faites cela en utilisant une grammaire telle que:
STATEMENT: = ASIGN_EXPRESSION | IF_STATEMENT
IF_STATEMENT: = IF, PAREN_EXPRESSION, STATEMENT
ASIGN_EXPRESSION: = IDENTIFIANT, ASIGN_OP, VALEUR
PAREN_EXPRESSSION: = OPEN_PAREN, VALUE, CLOSE_PAREN
VALEUR: = IDENTIFIANT | STRING_LITERAL | PAREN_EXPRESSION
ASIGN_OP: = EQUAL | ASIGN_ADD | ASIGN_SUBTRACT | ASIGN_MULT
Les productions qui utilisent "|" entre les termes signifie "correspond à l'un de ces", s'il y a des virgules, il signifie "correspond à cette séquence de termes"
Comment utilisez-vous cela? En commençant par le premier jeton, essayez de faire correspondre votre séquence de jetons à ces productions. Donc, vous essayez d’abord de faire correspondre votre liste de jetons avec STATEMENT, de sorte que vous lisiez la règle pour STATEMENT et qu’elle indique "un STATEMENT est soit un ASIGN_EXPRESSION, soit un IF_STATEMENT". Vous essayez donc de faire correspondre ASIGN_EXPRESSION en premier. Vous devez donc rechercher la règle de grammaire de ASIGN_EXPRESSION. et il indique "ASIGN_EXPRESSION est un IDENTIFIANT suivi d'un ASIGN_OP suivi d'une VALEUR, de sorte que vous recherchez la règle de grammaire pour IDENTIFIER et que vous voyez qu'il n'y a pas de syntaxe grammaticale pour IDENTIFIER, ce qui signifie IDENTIFIANT un" terminal ", ce qui signifie qu'il ne nécessite pas davantage analyser pour le faire correspondre afin que vous puissiez le faire directement avec votre jeton, mais votre premier jeton source est un IF, et si IF n'est pas identique à un IDENTIFIANT, la correspondance a échoué. Et maintenant? Vous revenez à la règle STATEMENT et essayez de faire correspondre le terme suivant: IF_STATEMENT. Vous regardez IF_STATEMENT, il commence par IF, lookup IF, IF est un terminal, comparez le terminal avec votre premier jeton, des correspondances de jetons IF, continuez, le terme suivant est PAREN_EXPRESSION, recherchez PAREN_EXPRESSION, ce n'est pas un terminal. PAREN_EXPRESSION commence par OPEN_PAREN, recherchez OPEN_PAREN, c'est un terminal, faites correspondre OPEN_PAREN à votre prochain jeton, cela correspond, etc., etc.
La méthode la plus simple pour aborder cette étape consiste à utiliser une fonction appelée parse () à laquelle vous transmettez le jeton de code source que vous essayez de faire correspondre et le terme de grammaire avec lequel vous essayez de le faire correspondre. Si le terme de grammaire n'est pas un terminal, vous appelez ensuite parse () en lui renvoyant le même jeton source et le premier terme de cette règle de grammaire. C’est pourquoi on appelle cela un "analyseur de descente récursif". La fonction parse () renvoie (ou modifie) votre position actuelle lors de la lecture des jetons source, elle renvoie essentiellement le dernier jeton de la séquence correspondante et vous continuez le prochain appel à parse () à partir de là.
Chaque fois que parse () correspond à une production telle que ASIGN_EXPRESSION, vous créez une structure représentant ce morceau de code. Cette structure contient des références aux jetons source d'origine. Vous commencez à construire une liste de ces structures. Nous appellerons toute cette structure l'arbre de syntaxe abstraite (AST)
Compiler et / ou exécuter : pour certaines productions de votre grammaire, vous avez créé des fonctions de gestionnaire qui, si elles disposaient d'une structure AST, compileraient ou exécuteraient cette partie d'AST.
Alors regardons le morceau de votre AST qui a le type ASIGN_ADD. Donc, en tant qu'interprète, vous avez une fonction ASIGN_ADD_execute (). Cette fonction est transmise en tant que partie de l'AST correspondant à l'arborescence d'analyse pour foo += "a string"
. Cette fonction examine donc cette structure et sait que le premier terme de la structure doit être un IDENTIFIANT, et que le second terme est la VALEUR. ASIGN_ADD_execute () passe le terme VALUE à une fonction VALUE_eval () qui renvoie un objet représentant la valeur évaluée en mémoire, puis ASIGN_ADD_execute () effectue une recherche sur "foo" dans votre table de variables et stocke une référence à tout ce qui a été renvoyé par eval_value () une fonction.
C'est un interprète. Au lieu de cela, un compilateur aurait des fonctions de gestionnaire traduisant l'AST en code d'octet ou en code machine au lieu de l'exécuter.
Les étapes 1 à 3, et certaines 4, peuvent être facilitées avec des outils tels que Flex et Bison. (alias Lex et Yacc) mais écrire un interprète vous-même est probablement l'exercice le plus stimulant qu'un programmeur puisse réaliser. Tous les autres défis en matière de programmation semblent triviaux après le sommet de celui-ci.
Mon conseil est de commencer petit: une langue minuscule, avec une grammaire minuscule, et essayez d'analyser et d'exécuter quelques instructions simples, puis progressez à partir de là.
Lisez-les et bonne chance!
http://www.iro.umontreal.ca/~felipe/IFT2030-Automne2002/Complements/tinyc.c
http://en.wikipedia.org/wiki/Recursive_descent_parser
lex
,yacc
etbison
.