Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

cours compilation.pdf

Tables des matières Tables des matières Introduction à la compilation ........................................................................... 3 1.1. Définition d’un compilateur ........................................................................................... 3 1.2. Compilation par analyse et synthèse .............................................................................. 4 1.2.1. Analyse du programme source................................................................................ 4 1.2.2. Phases d'un compilateur ........................................................................................... 5 1.2.3. Partie Frontale et finale ............................................................................................ 8 Expressions régulières et automates à états finis ........................................ 10 2.1. Expressions Régulières ................................................................................................ 10 2.2. Automates à états finis ................................................................................................. 12 2.2.1. Automates à états finis non déterministes .............................................................. 12 2.2.2. Automates à états finis déterministes ..................................................................... 14 2.2.3. Transformation d'un AFN en AFD......................................................................... 15 2.3. Transformation d'une expression régulière en un AFD .............................................. 18 2.3.1. Construction d'un AFN à partir d'une expression régulière ................................... 18 2.4. Simulation d'un AFN.................................................................................................... 23 Analyse lexicale ................................................................................................ 25 3.1. Le rôle de l'analyseur lexical........................................................................................ 25 3.1.1. Pourquoi une analyse lexicale ................................................................................ 26 3.1.2. Unités lexicales, modèles et lexèmes ..................................................................... 26 3.1.3. Attributs des unités lexicales.................................................................................. 27 3.2. Mémorisation du texte d'entrée: couple de tampons .................................................... 28 3.3. Spécification des unités lexicales................................................................................. 29 3.3.1. Chaînes et langage.................................................................................................. 29 3.3.2. Opération sur les langages...................................................................................... 30 3.4. Reconnaissances des unités lexicales........................................................................... 30 3.4.1. Diagrammes de transition....................................................................................... 31 3.5. Un langage pour spécifier des analyseurs lexicaux (lex) ............................................. 35 3.6. Conception d'un générateur d'analyseurs lexicaux....................................................... 36 3.6.1. Reconnaissance fondée sur les Automates finis non déterministes ....................... 37 3.6.2. Reconnaissance fondée sur les Automates finis déterministes .............................. 39 Analyse syntaxique .......................................................................................... 41 4.1. Grammaires non contextuelles ..................................................................................... 41 4.1.1. Définition d'une grammaire non contextuelle ........................................................ 42 4.1.2. Définition d'une Dérivation................................................................................... 42 4.2. Arbres d’analyse et dérivations .................................................................................... 44 4.2.1. Ambiguïté............................................................................................................... 45 Associativité des opérateurs ............................................................................................. 45 4.2.2. Priorités des opérateurs .......................................................................................... 45 Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 1 Tables des matières 4.3. Analyse descendante (TOP-DOWN) ........................................................................... 46 4.3.1. Analyse par descente récursive .............................................................................. 46 4.3.2. Analyse prédictive non récursive ........................................................................... 49 4.3.3. Diagrammes de transition pour analyseurs prédictifs ............................................ 55 4.3.4. Grammaires LL(1).................................................................................................. 57 4.4. Analyse ascendante (décalage/réduction) .................................................................... 59 4.4.1. Définition d'un Manche.......................................................................................... 60 4.4.2. Elagage du manche................................................................................................. 62 4.4.3. Implantation à l'aide d'une pile de l'analyse par décalage-réduction...................... 62 4.4.4. Analyseurs LR........................................................................................................ 63 4.4.5. Construction des tables d'analyse SLR à partir d'une grammaire .......................... 68 Traduction dirigée par la syntaxe .................................................................. 77 5.1. Définitions dirigées par la syntaxe ............................................................................... 78 5.1.2. Attributs synthétisés ............................................................................................... 78 5.1.3. Parcours en profondeur .......................................................................................... 79 5.2. Schéma de traduction ................................................................................................... 80 5.3. Arbre abstrait et arbre concret...................................................................................... 82 Contrôle de type ............................................................................................... 84 6.1. Expression de type ....................................................................................................... 84 6.2. Spécification d’un contrôleur de type simple............................................................... 85 6.2.1. Un langage simple .................................................................................................. 85 6.2.2. Contrôle de type des expressions ........................................................................... 86 6.2.3. Contrôle de type des instructions ........................................................................... 87 6.3. Conversion de type....................................................................................................... 88 Production de code intermédiaire ................................................................. 91 7.1. Code à trois adresses .................................................................................................... 91 7.1.1. Traduction dirigée par la syntaxe pour la production du code à trois adresses...... 91 7.1.2. Implantation d'instructions à trois adresses............................................................ 93 7.2. Expressions booléennes (méthode numérique) ............................................................ 94 7.3. Techniques de reprise Arrière (BackPatching) ............................................................ 96 7.3.1. Traduction des expressions booléennes ................................................................. 97 7.3.2. Traduction des instructions de flot de contrôle ...................................................... 98 7.3.3. Traduction des déclarations................................................................................. 101 Bibliographie ................................................................................................... 101 Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 2 Introduction à la compilation Introduction à la compilation Ce polycopié est le support de cours de « Techniques de compilation » présenté aux classes de deuxième année des études d’ingénieurs en Informatique à l’ENSI. Ce polycopié est inspiré de plusieurs cours et d’ouvrages de compilation, à savoir : - « Le cours de techniques de compilations » du Professeur Adbelfatteh BELGUITH, proposé aux étudiants de deuxième année à l’ENSI. - « Le cours de langages et compilation » du Docteur Lamia ELABED, proposé aux étudiants de troisième année à l’ISG de Tunis. - A. Aho, R. Sethi, J. Ullman, « Compilateurs : principes, techniques et outils », 2000. - J. Voiron, « Comprendre la compilation », (disponible à la bibliothèque de l’ENSI sous la référence : A-95) - H. Glaire, « Technique de compilation », 1989. (disponible à la bibliothèque de l’ENSI sous la référence : A-297) Dans ce cours, nous allons essayer d’aborder les principaux aspects des compilateurs. Pour approfondir les notions présentées dans ce cours, il est possible de consulter des ouvrages plus approfondis (voir bibliographie). Ce cours suppose que les étudiants ont des connaissances dans la matière « Théorie des langages ». 1.1. Définition d’un compilateur Un compilateur est un programme qui lit un programme écrit dans un premier langage -le langage source- et le traduit en un programme équivalent écrit dans un autre langage, le langage cible (voir figure 1.1). Au cours du processus de traduction, l'un des rôles importants des compilateurs est de signaler à l'utilisateur la présence d'erreurs dans le programme source. Figure 1.1. Un compilateur Il existe des milliers de langages source, allant des langages de programmation traditionnels comme Fortran, Pascal, C, etc. aux langages spécialisés, qui sont Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 3 Introduction à la compilation apparus dans quasiment tous les domaines d’applications de l’informatique. Les langages cibles sont au moins aussi variés; un langage cible peut être un langage de programmation ou bien le langage machine de n’importe quel ordinateur. 1.2. Compilation par analyse et synthèse Il y'a deux parties dans la compilation: - La partie analyse: partitionne le programme source en ses constituants et en crée une représentation intermédiaire. (Elle vérifie avant si le programme est bien écrit selon les spécifications du programme source). - La partie synthèse: construit le programme cible désiré à partir de cette représentation intermédiaire. 1.2.1. Analyse du programme source Cette analyse comprend 3 phases: 1. L'analyse linéaire (analyse lexicale): le flot de caractères formant le programme source est lu de gauche à droite et groupé en unités lexicales qui sont des suites de caractères ayant une signification collective. Cette phase aussi répond à la question: est-ce que les mots (jetons) qui existent dans ce programme source sont corrects? On utilise alors les automates à états finis déterministe et non déterministes. 2. L'analyse hiérarchique (analyse grammaticale ou syntaxique = Parsing): Cette phase répond à la question: est ce que la séquence de ces mots (résultant de l'analyse linéaire ) forme bien des phrases cohérentes, si oui, ces unités lexicales seront groupées hiérarchiquement dans des collections imbriquées ayant une signification collective (arbre syntaxique, Parse tree) Exemple 1.1. Un arbre décrivant une instruction d'affectation E := m*c*c := E * m * c c 3. L'analyse sémantique: au cours de laquelle on opère certains contrôles pour s'assurer que l'assemblage des constituants d'un programme a un sens. Elle répond aux questions: Est-ce que les opérandes de toutes les opérations sont correctes (types), faut-il faire une conversion, est-ce que les indices d'un tableau sont corrects. Si oui, extraire un programme écrit dans un autre langage = Traduction. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 4 Introduction à la compilation 1.2.2. Phases d'un compilateur Un compilateur opère en phases, chacune d'elles transformant le programme source d'une représentation en une autre comme le montre la figure 1.2: Exemple 1.2. (Analyse lexicale) Les caractères formant l'instruction d'affectation : position := initiale + vitesse * 60 seraient groupées dans les unités lexicales suivantes: 1. L'identificateur position 2. Le symbole d'affectation := 3. L'identificateur initiale 4. Le signe + 5. L'identificateur vitesse 6. Le signe de multiplication * 7. Le nombre 60 Les blancs séparent les caractères formant ces unités lexicales seront éliminées au cours de l'analyse lexicale. Remarques 1. • • • Une fonction essentielle d'un compilateur est d'enregistrer les identificateurs utilisés dans le programme source et de collecter de l'information sur divers attributs de chaque identificateur. Ces attributs fournissent de l'information concernant, par exemple, l'emplacement mémoire assigné à un identificateur, son type, sa portée. Une table de symboles est une structure de données contenant un enregistrement pour chaque identificateur, muni de champs pour chaque identificateur. Cette structure de données permet de retrouver rapidement l'enregistrement correspondant à un certain identificateur et d'accéder rapidement aux données qu'il contient. Quand l'analyseur lexical détecte un identificateur, il le fait entrer dans la table des symboles, cependant ses attributs ne peuvent normalement pas être déterminés pendant l'analyse lexicale; par exemple, le type ne sera déterminé que pendant l'analyse sémantique, il sera donc ajouté durant cette phase. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 5 Introduction à la compilation Figure 1.2. Phases d'un compilateur Remarque 2. Détection et compte rendu des erreurs Chaque phase peut rencontrer des erreurs. La phase d'analyse lexicale peut détecter des erreurs quand les caractères restant à lire ne peuvent former aucune unité lexicale du langage. La phase d'analyse syntaxique détecte les erreurs dues au fait que le flot d'unités lexicales n'est pas conforme aux règles structurelles (syntaxe) du langage. Dans la phase d'analyse sémantique, le compilateur détecte les constructions ayant une structure grammaticale correcte, mais telle que les opérations qu'elles Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 6 Introduction à la compilation mettent en œuvre n'ont pas de sens. Par exemple lorsqu'on essaye d'additionner deux identificateurs; l'un est le nom d'un tableau et l'autre celui d'une procédure. Remarque 3. Analyse syntaxique Cette phase consiste à regrouper les unités lexicales du programme source en structures grammaticales qui seront employées par le compilateur pour synthétiser son résultat. Ces phases sont en général représentées par un arbre syntaxique comme celui de la figure 1.3. Instruction d'affectation identificateur expression := position + expression expression identificateur initiale expression * identificateur expression nombre 60 vitesse Figure 1.3. Arbre syntaxique pour : position := initiale + vitesse * 60 Les structures syntaxiques d'un langage sont généralement exprimées par des règles récursives (grammaire) G( V, ∑, (terminaux + non terminaux, alphabet, R, règles de production, S) axiome) A → id := E E →E+E E →E *E E →id E →nb Remarque 4. Génération du code Intermédiaire On peut considérer le code Intermédiaire comme un programme pour une machine qui a une infinité de registres. Cette représentation doit avoir deux propriétés importantes; Elle doit être facile à produire à partir de l'arbre syntaxique et facile à traduire en langage cible. Dans ce cours nous étudierons une forme intermédiaire appelée "code à 3 adresses" qui est semblable au langage d'assemblage d'une machine dans laquelle chaque Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 7 Introduction à la compilation emplacement peut jouer le rôle d'un registre. Un fragment de code à 3 adresses consiste en une séquence d'instructions, dont chacune a au plus 3 opérandes. Le programme source "position := initiale + vitesse * 60 pourrait être traduit en code à trois adresses de la façon suivante: temp1 := EntierVersRéel (60) temp2 := id3 * temp1 temp3 := id2 + temp2 id1 := temp3 Remarque 5. Optimisation du code Cette phase tente d'améliorer le code intermédiaire de façon que le code machine résultant s'exécute plus rapidement. Exemple 1.3. temp1 := EntierVersRéel (60) temp2 := id3 * temp1 temp3 := id2 + temp2 id1 := temp3 la conversion de 60 peut être faite une fois pour toutes au moment de la compilation (60.0) temp3 n'est utilisée qu'une seule fois pour transmettre sa valeur à id1 on peut alors substituer id1 à temp3 on obtient donc: temp1 := id3 * 60.0 id1 := id2 + temp1 Remarque 6. Production du code La phase finale d'un compilateur est la production du code cible. Un aspect crucial de ce processus est l'assignation de variables aux registres. Exemple 1.4. pour le code intermédiaire temp1 := id3 * 60.0 id1 := id2 + temp1 on obtient le code objet suivant: LOAD id3, R1 MULF #60.0, R1 LOAD id2,R2 ADD R1, R2 STORE R1, id1 charge le contenu de id3 dans le register R1 MULF: multiplication en virgule flottante, #: constante additionne le contenu de R1 et R2, le résultat est place dans R1 copie le contenu de R1 dans l'emplacement id1 1.2.3. Partie Frontale et finale La construction d'un compilateur se divise généralement en deux parties: une partie frontale et une partie finale. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 8 Introduction à la compilation - La partie frontale est constituée des phases qui dépendent principalement du langage source et qui sont en grande partie indépendantes de la machine cible. Elle comprend normalement l'analyse lexicale, l'analyse syntaxique, la création de la table des symboles, l'analyse sémantique et la production du code intermédiaire. La partie frontale peut aussi effectuer une certaine quantité d'optimisation du code. Elle inclut aussi le traitement d'erreurs associé à chacune de ces phases. - La partie finale comprend les portions du compilateur qui dépendent de la machine cible; celles ci ne dépendent pas du langage source, mais seulement du langage intermédiaire. On trouve dans la partie finale certains aspects de l'optimisation du code ainsi que la production de code avec les opérations nécessaires de traitement d'erreurs et de gestion de la table des symboles. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 9 Expressions régulières et automates à états finis Expressions régulières et automates à états finis 2.1. Expressions Régulières Le problème préliminaire qui se pose à propos des langages est : comment décrire un langage ? L’étape suivante sera : étant donnée une phrase (un mot), est-ce qu’elle appartient au langage ? Dans cette partie, nous introduisons quelques notions de base de la théorie des langages. En particuliers, nous décrivons les expressions régulières qui permettent de décrire un certain type de langages, que l’on appelle langages réguliers. En Pascal, un identificateur est une lettre suivie d'un nombre quelconque, éventuellement nul, de lettres ou de chiffres. Une expression régulière est une notation qui nous permettra de définir avec précision les ensembles de ce genre. Avec cette notation, on peut définir les identificateurs de Pascal comme : lettre (lettre|chiffre)* La barre verticale signifie "ou", les parenthèses sont utilisées pour grouper des sousexpressions, l'étoile signifie "un nombre quelconque, éventuellement nul, d'instances de" l'expression entre parenthèses, et la juxtaposition de lettre au reste de l'expression signifie la concaténation. On construit une expression régulière à partir d'expressions régulières plus simples en utilisant un ensemble de règles de définition. Chaque expression régulière r dénote un langage L(r). Les règles de définition spécifient comment L(r) est formé en combinant de manières variées les langages dénotées par les sous expressions de r. Voici les règles qui définissent les expressions régulières sur un alphabet Σ. Associé à chaque règle figure une spécification du langage dénoté par l'expression régulière ainsi définie. 1. ε est une expression régulière qui dénote {ε}, c'est à dire l'ensemble constitué de la chaîne vide. 2. Si a est un symbole de Σ, alors a est une expression régulière qui dénote {a}, c'est-à-dire l'ensemble constitué de la chaîne a. D'après le contexte on peut parler de a comme expression régulière, comme chaîne ou comme symbole. 3. Supposons que r et s soient des expressions régulières dénotant les langages L(s) et L(r), alors : a - (r) | (s) est une expression régulière dénotant L(r) ∪ L(s). b - (r) (s) est une expression régulière dénotant L(r) L(s). Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 10 Expressions régulières et automates à états finis c - (r)* est une expression régulière dénotant (L(r))*. d - (r) est une expression régulière dénotant L(r), c'est-à-dire qu’on peut placer des paires de parenthèses excédentaires autour d'expressions régulières si on le désire. Remarques 1. L'opérateur unaire « * » à la plus haute priorité et est associatif à droite. 2. La concaténation a la deuxième plus haute priorité et est associative à gauche. 3. L’opérateur « | » a la plus faible priorité et est associatif à gauche. Selon les conventions, (a) | ((b)* (c)) est équivalente à a|b*c. Les deux expressions dénotent l'ensemble des chaînes qui ont soit un seul a, soit un nombre quelconque éventuellement nul, de b suivis par un c. Exemple 2.1. Soit Σ = {a, b} 1. L'expression régulière a|b dénote l'ensemble {a, b} 2. L'expression régulière (a|b) (a|b) dénote {aa, ab, ba, bb}, l'ensemble de toutes les chaînes de a et de b de longueur deux. Une autre expression régulière pour ce même ensemble est : aa | ab | ba | bb. 3. L'expression régulière a * dénote l'ensemble de toutes les chaînes formées d'un nombre quelconque (éventuellement nul) de a, c'est à dire {ε, a, aa, aaa, …}. 4. L'expression régulière (a|b)* dénote l'ensemble de toutes les chaînes constituées d'un nombre quelconque (éventuellement nul) de a ou de b. Une autre expression régulière pour cet ensemble est (a*b* ) *. 5. L'expression régulière a|a*b dénote l'ensemble contenant la chaîne a et toute les chaînes constituées d'un nombre quelconque (éventuellement nul) de a suivi d'un b. Si deux expressions régulières r et s dénotent le même langage, on dit que r et s sont équivalentes et on écrit r = s. Par exemple, (a | b) = (b | a). Les expressions régulières obéissent à un certain nombre de lois algébriques qui peuvent être utilisées pour manipuler les expressions régulières sous des formes équivalentes, le tableau 2.1 montre quelques lois algébriques qui s'appliquent aux expressions régulières r, s, t. Axiome Description r|s = s|r r | (s|t) = (r|s) | t « | » est commutatif « | » est associatif (rs)t = r(st) r(s|t) = rs | rt (s|t)r = sr|tr εr = r La concaténation est associative La concaténation est distributive par rapport à «|» ε est l'élément neutre pour la concaténation Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 11 Expressions régulières et automates à états finis rε = r r* = (r|ε)* Relation entre * et Σ r**= r* * est idempotent Tableau 2.1. Propriétés algébriques des expressions régulières 2.2. Automates à états finis Le problème qui se pose est de pouvoir reconnaître si un mot donné appartient à un langage donné. Un reconnaisseur pour un langage est un programme qui prend en entrée une chaîne x et répond oui si x est une phrase (mot) du langage et non sinon. Les automates à états finis (A.E.F.) sont des reconnaisseurs pour les langages réguliers. Un automate fini peut être déterministe ou non déterministe, ce dernier terme signifiant qu'on peut trouver plus d'une transition sortant d'un état sur le même symbole d'entrée. ⇒ Il y a conflit temps/place : - Les automates finis déterministes produisent des connaisseurs plus rapides que les automates finis non déterministes. - Un automate fini déterministe peut être beaucoup plus volumineux qu'un automate fini non déterministe. 2.2.1. Automates à états finis non déterministes Un automate fini non déterministe (AFN en abrégé) est un modèle mathématique qui consiste en : 1. Un ensemble d'états E ; 2. Un ensemble de symboles d'entrées Σ (l'alphabet des symboles d'entrée); 3. Une fonction Transiter, qui fait correspondre des couples état-symbole à des ensembles d'états. 4. Un état e0 qui est distingué comme état de départ ou état initial 5. Un ensemble d'états F distingués comme états d'acceptation ou états d'acceptation ou états finals. Un AFN peut être représentée graphiquement comme un graphe orienté étiqueté, appelé graphe de transition, dans lequel les nœuds sont les états et les arcs étiquetés représentent la fonction de transition. Remarque. Ce graphe ressemble à un diagramme de transition, mais le même caractère peut étiqueter deux transitions au plus en sortie d'un même nœud et les arcs peuvent être étiquetés par le symbole spécifique au même titre que les symboles d'entrées. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 12 Expressions régulières et automates à états finis Exemple 2.2. Soit le langage dénoté par l'expression régulière (a | b)* abb, consistant en l'ensemble des chaînes de a et de b se terminant par abb. La figure 2.1 représente le graphe de transition par un AFN qui reconnaît ce langage. L'ensemble des états de l'AFN est {0, 1, 2, 3} et l'alphabet d'entrée est {a, b}. L'état 0 est distingué comme étant l'état de départ et l'état d'acceptation 3 est indiqué par un double cercle. a a 0 début b 1 b 2 3 b Figure 2.1. Un automate fini non déterministe Implémentation d’un AFN En machine la fonction de transition d'un AFN peut être implantée à l'aide d'une table de transition dans laquelle il y a une ligne pour chaque état et une colonne pour chaque symbole d'entrées et ε, si nécessaire. L'entrée pour la ligne i et le symbole a dans la table, donne l'ensemble des états qui peuvent être atteints par une transition depuis l'état i sur le symbole a. (voir tableau 2.2) SYMBOLE D'ENTREE a b {0, 1} {0} {2} {3} - Etats 0 1 2 3 Tableau 2.2. Table de transition pour l'automate fini non déterministe de la figure 2.1 Un AFN accepte une chaîne d'entrée x si et seulement si il existe un certain chemin dans le graphe de transition entre l'état de départ et un état d'acceptation, tel que les étiquettes d'arcs le long de ce chemin épellent le mot x. L'AFN de la figure 2.1 accepte les chaînes d'entrées abb, aabb, babb, aaabb, … Par exemple, aabb est acceptée par le chemin depuis 0, en suivant de nouveau l'arc étiqueté a vers l'état 0, puis vers les états 1, 2 et 3 via les arcs étiquetés a;b et b respectivement. Un chemin peut être représenté par une suite de transitions d'état appelée déplacements. Ce diagramme montre les déplacements réalisés en acceptant la chaîne d'entrée aabb. a 0 a 0 b 1 Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 b 2 3 En général, il existe plus d'une suite de déplacements pouvant mener à l'état d'acceptation 13 Expressions régulières et automates à états finis Bien d'autres suites de déplacements pourraient être faites sur la chaîne d'entrée aabb, mais aucune des autres n'arrive à terminer dans un état d'acceptation. a 0 a 0 b 0 b 0 Cette suite stationne dans l'état de non acceptation 0. 0 Æ Le langage défini par un AFN est l'ensemble des chaînes d'entrées qu'il accepte. Exemple 2.3. Soit l’expression régulière aa*|bb*. Cette expression est acceptée par l’automate de la figure 2.4. a a 1 ε 2 0 ε b 3 b 4 Figure 2.4. Automate fini non déterministe acceptant aa*|bb* La chaîne aaa est acceptée en se déplaçant via les états 0, 1, 2, 2, et 2. Les étiquettes de ces arcs sont : ε, a, a, et a dont la concaténation est aaa. Notons que ε disparaît dans la concaténation. 2.2.2. Automates à états finis déterministes Un automate fini déterministe (AFD en abrégé) est un cas particulier d'automate fini non déterministe dans lequel : 1. Aucun état n'a de ε transitions, c'est à dire de transition sur l'entrée ε et 2. Pour chaque état e et chaque symbole d'entrée a, il y a au plus un arc étiqueté a qui quitte e. Un automate fini déterministe a au plus une transition à partir de chaque état sur n'importe quel symbole. Chaque entrée de sa table de transition est un état unique. Il est très facile de déterminer si un automate fini déterministe accepte une chaîne d'entrée, puisqu'il existe au plus un chemin depuis l'état de départ étiqueté par cette chaîne. L'algorithme 2.1 montre comment simuler le comportement d'un AFD sur une chaîne d'entrée. Algorithme 2.1. Simulation d'un AFD Données : Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 14 Expressions régulières et automates à états finis - Une chaîne d'entrée x terminée par un caractère de fin de fichier « fdf ». - Un AFD D avec un état de départ e0 et un ensemble d'états d'acceptation F. Résultat : La réponse "oui" si D accepte x; "non" dans le cas contraire. Méthode : La fonction transiter(e, c) donne l'état vers lequel il y a une transition depuis l'état e sur le caractère d'entrée c. La fonction CarSuiv retourne le prochain caractère de la chaîne d'entrée x. e := e0 C:=CarSuiv(); Tanque c ≠ fdf faire e:= Transiter(e, c); c:= CarSuiv(); fin si e ∈ F alors retourner "oui" sinon retourner "non" Exemple 2.4. La figure 2.5 présente le graphe de transition d'un automate fini déterministe qui accepte la langage (a|b)* abb (le même que celui accepté par l'AFN de la figure 2.1). Avec cet AFD et la chaîne d'entrée ababb, l'algorithme 1 passe par la suite les états 0, 1, 2, 1, 2 et 3 et retourne oui. Figure 2.5. DFA acceptant (a|b)*abb 2.2.3. Transformation d'un AFN en AFD Si on utilise un AFN pour vérifier si une chaîne x ∈ langage, on trouvera plusieurs chemins qui épellent la même chaîne x, on doit les avoir tous pris en compte avant d'en trouver un qui mène à l'acceptation ou de découvrir qu'aucun d'entre eux ne mène à un état d'acceptation. Remarque. Dans la table de transition d'un AFN, chaque entrée est un ensemble d'états. Dans la table de transition d'un AFD chaque entrée est un état unique. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 15 Expressions régulières et automates à états finis L'idée ⇒ chaque état de l'AFD correspond à un ensemble d'états de l'AFN. Algorithme 2.2. Construction d'un AFD à partir d'un AFN Données : Un AFN N Résultat : Un AFD D qui accepte le même langage. Méthode : Notre algorithme construit une table de transition Dtran pour D. Chaque état de l'AFD est un ensemble d'états de l'AFN et on construit Dtran de telle manière que D simulera "en parallèle" tous les déplacements possibles que N peut effectuer sur une chaîne d'entrée donnée. On utilise les opérations suivantes (voir tableau 2.3) pour garder trace des ensembles d'états de l'AFN (e représente un état de l'AFN et T représente un ensemble d'états de l'AFN). opération ε-fermeture(e) ε-fermeture(T) Transiter(T, a) Description Ensemble des états de l'AFN accessibles depuis un état e de l'AFN par de ε-transitions uniquement Ensemble des états de l'AFN accessibles de puis un état e appartenant à T par des ε-transitions uniquement Ensemble des états de l'AFN vers lesquels il existe une transition sur le symbole à partir d'un état e appartenant à T Tableau 2.3. Opérations sur les états d'un AFN Avant de voir le premier symbole d'entrée, N peut appartenir à n'importe lesquels des états de l'ensemble ε-fermeture (e0) ou e0 est l'état de départ de N. Supposons que seules les états de l’ensemble T soient accessibles à partir de e0 sur une suite donnée de symboles d’entrée, et soit a le prochain symbole d’entrée. Quand il voit a, N peut passer dans un des états de l’ensemble Transiter (T, a). Quand on autorise des εtransitions, N peut être dans un des états de ε-fermeture (Transiter (T, a)), après avoir lu le a. On construit Détats, l'ensemble des états de D et Dtran, la table de transition de D, de la manière suivante. Chaque état de D correspond à un ensemble d'états de l'AFN dans lesquels N pourrait se trouver après avoir lu une suite de symboles d'entrée, en incluant toutes les ε-transitions possibles avant ou après la lecture des symboles. L'état de départ D est ε-fermeture(e0). On ajoute des états et des transitions à D en utilisant l'algorithme 2.3. Algorithme 2.3. Construction des sous-ensembles Au départ ε-fermeture(e0) est l'unique état de Détats et il est non marqué; Tant que il existe un état non marqué T dans Détats faire début Marquer T; Pour chaque symbole d'entrée a faire début U : = ε-fermeture(Transiter(T, a)); Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 16 Expressions régulières et automates à états finis Si U n'appartient pas à Détats alors Ajouter U comme nœud non marqué de Détats; Dtran[T,a] : = U Fin Fin Un état D est un état d'acceptation si c'est un ensemble d'états de l'AFN qui contient au moins un état d'acceptation. Exemple 2.5. La figure 2.6 illustre un AFN qui accepte le langage (a|b)*abb. Appliquons l'algorithme 2.2 à cet AFN. L'état de départ de l'AFD équivalent est ε-fermeture(0), qui est A = {0,1,2,4,7}, étant donné que ce sont précisément les états accessibles depuis l'état 0 via un chemin dans lequel chaque arc est étiqueté ε. ε a 0 ε ε 1 2 ε 3 ε b 4 ε ε 6 ε a 7 b 8 b 9 10 5 Figure 2.6. Un NFA pour le langage (a|b)*abb L'alphabet des symboles d'entrée est ici {a,b}. L'algorithme 2.3 nous dit de marquer A et de calculer ε-fermeture(Transiter(A, a)). Nous calculons d'abord Transiter (A,a), l'ensemble des états de N qui ont des transitions sur a depuis les éléments de A. Parmi les états 0, 1, 2, 4 et 7, seuls 2 et 7 ont de telles transitions vers 3 et 8, aussi : ε-fermeture(Transiter({0,1,2,4,7},a) = ε-fermeture({3,8}) = {1,2,3,4,6,7,8} Appelons cet ensemble B. Nous avons alors Dtran[A,a]= B. Parmi les états de A, seul 4 a une transition sur b vers 5 aussi l'AFD a une transition sur b depuis A vers : C = ε-fermeture({5}) = {1,2,4,5,6,7}. Donc Dtran[A,b] = C Si nous continuons ce processus avec les ensembles actuellement non marqués B et C, on atteint finalement le point où tous les ensembles qui sont des états de l'AFD sont marqués. Les cinq différents ensembles que nous construisons réellement sont : A = { 0,1,2,4,7} D = {1,2,4,5,6,7,9} Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 17 Expressions régulières et automates à états finis B = {1,2,3,4,6,7,8} E = {1,2,4,5,6,7,10} C = {1,2,4,5,6,7} L'état A est l'état de départ et l'état E est l'unique état d’acceptation (le tableau 2.4 donne la table du transition complète). Etat A B C D E a B B B B B Symbole d'entrée b C D C E C Tableau 2.4. Table de transition Dtran pour le DFA Voici le graphe de transition correspondant à l'AFD (voir figure 2.7) Figure 2.7. DFA pour (a|b)*abb 2.3. Transformation d'une expression régulière en un AFD Une façon de construire un reconnaisseur à partir d'une expression régulière consiste à construire un NFA à partir d'une expression régulière ensuite simuler le comportement de l'NFA sur une chaîne d'entrée en utilisant les algorithmes que nous allons voir. Si la vitesse d'exécution est essentielle, on peut convertir le AFN en AFD. 2.3.1. Construction d'un AFN à partir d'une expression régulière L’algorithme est dirigé par la syntaxe en ce sens qu'il utilise la structure syntaxique de l'expression régulière pour guider le processus de construction. On va voir d'abord comment construire les automates pour les opérations qui contiennent un opérateur d'alternance, de concaténation ou de fermeture de Kleene. Par exemple, pour l'expression r|s, on construit un AFN à partir des AFN reconnaissant r et s. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 18 Expressions régulières et automates à états finis Algorithme 2.4. Construction d'un AFN à partir d'une expression régulière, ou construction de Thompson. Donnée : Une expression régulière r sur un alphabet ∑. Résultat : un AFN N qui reconnaît L(r). Méthode : On décompose d'abord r en ses sous expressions. Puis en utilisant les règles (1) et (2) ci dessous, on construit des NFA pour chacun des symboles de base de r, c'est à dire doit ε soit les symboles de l'alphabet. Les symboles de base correspondent aux parties (1) et (2) dans la définition d'une expression régulière (il est important de comprendre que si un symbole a apparaît plusieurs fois dans r, un AFN séparé est construit pour chaque occurrence). Ensuite, en se guidant sur la structure syntaxique de l'expression régulière r, on combine régulièrement ces AFN en utilisant la règle (3) ci dessous, jusqu'à obtenir le AFN pour l'expression régulière complète. Chaque AFN intermédiaire produit au cours de la construction correspond à une sous- expression de r et a plusieurs propriétés importantes: il a exactement un état final, aucun arc ne rentre dans l'état de départ et aucun arc ne quitte l'état final. 1. Pour ε, construire l'AFN: déb i ε f Ici i est un nouvel état de départ et f un nouvel état d'acceptation. Il est clair que cet AFN reconnaît {ε} 2. Pour un a appartenant à Σ, construire l'AFN : déb a i f Ici encore, i est un nouvel état de départ et f un nouvel état d'acceptation. Cet automate reconnaît {a}. 3. Supposons que N(s) et N(t) soient les AFN pour les expressions régulières s et t. (a) Pour l'expression régulière s|t, construire l'AFN composé suivant N(s|t): ε déb i ε N(s ε Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 f ε 19 Expressions régulières et automates à états finis Ici, i est un nouvel état de départ et f un nouvel état d'acceptation. Il y a une transition sur ε depuis i vers les états de départ de N(s). Il y a une transition sur ε depuis les états d'acceptation de N(s) et N(t) vers le nouvel état d'acceptation f. Les états de départ et d'acceptation N(s) et N(t) ne sont pas les états de départ et d'acceptation de N(s|t). Il est à noter que tout chemin depuis i vers f doit traverser soit N(s), soit N(t) exclusivement on voit donc que l'automate composé reconnaît L(s) ∪ L (t). (b) Pour l'expression régulière st, construire l'AFN composé N(st) : N(s i N(t f L'état de départ de N(s) devient l'état de départ de l'AFN composé et l'état d'acceptation de N(s) est fusionné avec l'état de départ de N(t). ⇒ Toutes les transitions depuis l'état de départ de N(t) deviennent des transitions depuis l'état d'acceptation de N(s). Le nouvel état fusionné perd son statut d'état de départ ou d'acceptation dans l'AFN composé. (c) Pour l'expression régulière S*, construire l'AFN composé N(S*) : ε i ε N(s ε f ε Ici, i est un nouvel état de départ et f un nouvel état d'acceptation. Dans l'AFN composé, on peut aller de i à f directement en suivant un arc étiqueté ε qui représente que ε appartient à (L(s))*, ou bien on peut aller de i à f en traversant N(s) une ou plusieurs fois. Il est clair que l'automate composé reconnaît (L(s))*. (d) Pour l'expression régulière parenthèse (s), utiliser N(s) lui même comme AFN. Chaque fois qu'on construit un nouvel état, on lui donne un nom distinct. Ainsi, il ne peut y avoir deux états dans deux sous-automates qui aient le même nom. Même si le même symbole apparaît plusieurs fois dans r, on crée, pour chaque instance de ce symbole, un AFN séparé avec ses propres états. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 20 Expressions régulières et automates à états finis La construction produit un AFN N(r) qui a les propriétés suivantes : 1. N(r) a au plus deux fois d'états qu'il y a de symboles et d'opérateurs dans r. Ce ci revient du fait que chaque étape de la construction crée au plus deux nouveaux états 2. N(r) a également un état de départ et un état d'acceptation. L'état d'acceptation n'a pas de transition sortante. Cette propriété s'applique de la même manière à chacun des sous automates le constituant. 3. Chaque état de N(r) a soit une transition sortante sur un symbole de Σ, soit au plus deux transitions sortantes sur ε. Exemple 2.6. Soit l'expression régulière r = (a|b)* abb. La figure 2.8 présente un arbre syntaxique pour r. Figure 2.8. Décomposition de (a|b)*abb - Pour le composant r1, le premier a, on construit l'AFN - Pour r2, on construit - On peut maintenant combiner N(r1) et N(r2) en utilisant la règle d'union pour obtenir l'AFN pour r3 = r1|r2 : Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 21 Expressions régulières et automates à états finis - L'AFN pour r4 est le même que celui pour r3. L'AFN pour (r4)* est alors : - L'AFN pour r6= a est : - Pour obtenir l'automate pour r7 =r5r6, on fusionne les états 7 et 7' en appelant l'état résultant 7 pour obtenir - En continuant ainsi on obtient l'AFN pour r11 = (a|b)*abb Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 22 Expressions régulières et automates à états finis Figure 2.9. AFN obtenu par la construction de Thompson pour r11=(a|b)*abb 2.4. Simulation d'un AFN Nous présentons maintenant un algorithme qui, si on lui donne un AFN N construit par l'algorithme 2.4 (construction de Thompson) et une chaîne d'entrée x, détermine si N accepte x. L'algorithme fonctionne en lisant le texte d'entrée, caractère par caractère, et en calculant l'ensemble complet des états dans lesquels N peut se trouver après avoir lu chaque préfixe de la chaîne d'entrée. Algorithme 2.5 : Simulation d'un AFN Données : Un AFN N construit par l'algorithme 4 et une chaîne d'entrée x, On suppose que x est terminée par un caractère de fin de fichier « fdf ». N a un état de départ e0 et un ensemble d'états d'acceptation F. Résultat : La réponse « oui » si N accepte x; « non » dans le cas contraire. Méthode : Appliquer l'algorithme suivant à la chaîne d'entrée x E := ε-fermeture({e0}); a:= Carsuiv(); Tant que a ≠ fdf faire début E:=ε-fermeture (transiter(E,a)); A:= CarSuiv() Fin Si E∩F ≠ Ø alors Retourner "oui Sinon Retourner "non" L'algorithme en réalité réalise la construction des sous-ensembles à l'exécution. Il calcule une transition depuis l'ensemble courant d'états E vers le prochain ensemble d'états en deux étapes. Tout d'abord, il détermine l'ensemble transiter(E, a)de tous les états qui peuvent être atteints depuis un état de E par une transition sur a, le caractère d'entrée courant. Ensuite, il calcule ε-fermeture de transiter(E, a), c'est à dire tous les états qui peuvent être atteints depuis transiter (E, a) par zéro ou plusieurs εfermeture. L'algorithme utilise la fonction CarSuiv pour lire les caractères de x, un par un. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 23 Expressions régulières et automates à états finis Quand tous les caractères de la chaîne d'entrée x ont été traités, l'algorithme retourne « oui » si l'ensemble des états courants contient un état d'acceptation, « non » dans le cas contraire. Implantation à l'aide de deux piles Cet algorithme peut être efficacement implanté en utilisant deux piles et un vecteur indicé par les états de l'AFN : - On utilise une pile pour garder trace de l'ensemble courant des états non déterministe, et l'autre pile pour calculer l'ensemble suivant d'états non déterministes. - On peut utiliser le vecteur de bits pour déterminer rapidement si un état non déterministe est déjà dans la pile, de manière à ne pas l'ajouter une deuxième fois. - Une fois qu'on a calculé l'état suivant sur la deuxième pile, on peut échanger le rôle de deux piles. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 24 Analyse lexicale Analyse lexicale Ici, on traite des techniques de spécification et d'implémentation d'analyseurs lexicaux. Une manière simple de construire un analyseur lexical consiste à bâtir un diagramme qui illustre la structure des unités lexicales du langage source, puis de traduire à la main le diagramme en programme qui reconnaît les unités lexicales. Un outil logiciel qui automatise la construction d'analyseurs lexicaux permet à des utilisateurs de cultures différentes d'employer des comparaisons de modèles, ou filtrage dans leurs propre domaines d'application. Par exemple, Javis a utilisé un constructeur d'analyseurs lexicaux pour créer un programme qui reconnaît des imperfections dans les circuits imprimés. Un avantage majeur d'un constructeur d'analyseurs lexicaux est qu'il peut utiliser les algorithmes de filtrage les plus connus et en conséquence, créer des analyseurs lexicaux efficaces pour des utilisateurs qui ne sont pas des experts en matière de technique de filtrage par modèle. 3.1. Le rôle de l'analyseur lexical L’analyseur lexical constitue la première phase d'un compilateur, sa tache principale est de lire les caractères d'entrée et de produire comme résultat, une suite d'unité lexicales que l'analyseur syntaxique va utiliser (voir figure 3.1). Figure 3.1. Interaction entre un analyseur lexical et un analyseur syntaxique A la réception d'une commande "prochaine unité lexicale" de l'analyseur syntaxique, l'analyseur lexical lit les caractères d'entrée jusqu'à ce qu'il puisse obtenir la prochaine unité lexicale. Cette interaction est couramment implantée en faisant de l'analyseur lexical un sous programme de l'analyseur syntaxique. L’analyseur lexical peut aussi réaliser certaines tâches secondaires comme l'élimination des commentaires et des espaces (caractère blanc, tabulation, fin de ligne). Une autre tâche consiste à relier les messages d'erreurs issus du compilateur, au programme source (garder la trace du nombre de caractère fin de ligne rencontrée pour pouvoir associer un numéro de ligne à un message d'erreur). Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 25 Analyse lexicale On divise souvent les analyseurs lexicaux en deux phases successives, la première appelée « lecture » et la seconde « analyse lexicale ». La lecture est responsables de tâches simples (exemple, éliminer les blancs), tandis que l’analyse lexicale proprement dit réalise les opérations les plus complexes. 3.1.1. Pourquoi une analyse lexicale De nombreuse raisons justifient de découper la phase d'analyse de la compilation en analyse lexicale et analyse syntaxique. 1. Une conception plus simple (simplifier l'une ou l'autre des phases). Exemple : un analyseur syntaxique contiendrait les traitements relatifs aux commentaires et aux espaces serait nettement plus complexe que celui qui les considère déjà éliminés par un analyseur lexical. 2. L'efficacité du compilateur est améliorée. Un analyseur lexical séparé permet de construire un module spécialisé et potentiellement plus efficace pour cette tache (utilisation de technique spécialisées de gestion des tampons au cours de la lecture des caractères d'entrée et du traitement des unités lexicales. 3. La portabilité du compilateur est accrue. Les particularités de l'alphabet d'entrées et d'autres anomalies spécifiques au matériel peuvent être confiées à l'analyseur lexical (la représentation des symboles spéciaux ou non standard, comme la ↑ en Pascal, peut être restreinte à l'analyseur lexical). Des outils spécialisés ont été conçus pour aider à automatiser la construction d'analyseurs lexicaux et syntaxiques (on va les étudier ultérieurement) 3.1.2. Unités lexicales, modèles et lexèmes Quand on parle d'analyse lexicale, on utilise les termes « unité lexicale » « modèle » et « lexème » avec des spécifications bien spécifiques. Le tableau 3.1 donne des exemples de leur utilisation. En général, il y a un ensemble de chaînes en entrée pour lesquelles la même unité lexicale est produite en sortie. Cet ensemble de chaînes est décrit par une règle appelée modèle associé à l'unité lexicale. On dit que le modèle filtre chaque chaîne de l'ensemble. Un lexème est une suite de caractères du programme source qui concorde avec le modèle de l'unité lexicale. Par exemple, dans la déclaration Pascal : const pi=3.1416. La sous-chaîne pi est un lexème de l'unité lexicale « identificateur ». Les lexèmes reconnus par le modèle décrivant l'unité lexicale représentent les chaînes de caractères du langage source qui peuvent être traitées en bloc comme une unité lexicale. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 26 Analyse lexicale const const Description informelle des modèles const if oprel (opérateur de relation) if if < <= = <> > >= < <= = <> > >= id pi compte D2 nb 3.1416 0 6.02E23 littéral "cours compilation" Unité lexicale Lexèmes lettre suivie de lettres ou chiffres Toute constante numérique Tous caractère entre"et" sauf " Tableau 3.1. Exemples d'unités lexicales Dans la plupart des langages de programmation, les constructions suivantes sont traitées comme des unités lexicales : mots clés, opérateurs, identificateurs, constantes, chaînes littérales et symbole de ponctuation (parenthèses, virgules, :). Dans l'exemple précédent, quand la suite de caractères pi apparaît dans le programme source, une unité lexicale représentant un identificateur est retournée à l'analyseur syntaxique (souvent en passant un entier correspondant à l'unité lexicale, c'est cet entier qui est référencé par l'unité lexicale id). Un modèle est une règle qui décrit l'ensemble des lexèmes pouvant représenter une unité lexicale particulière dans les programmes sources. Le modèle de l'unité lexicale const est limité à la simple chaîne const qui orthographie le mot clé. Le modèle de l'unité lexicale oprel est l'ensemble des six opérateurs de relation de pascal. Pour décrire précisément les modèles d'unités lexicales plus complexes, comme id et nb nous utiliserons la notation des expressions régulières que nous allons voir. 3.1.3. Attributs des unités lexicales L'analyseur lexical réunit les informations sur les unités lexicales dans les attributs qui leur sont associés. Par exemple le modèle nb correspond à la fois à la chaîne 0 et 1, mais il est essentiel pour le générateur de code de savoir quelle chaîne a effectivement été reconnue. Les unités lexicales influent sur les décisions de l'analyse syntaxique les attributs influent sur la traduction des unités lexicales. En pratique, une unité lexicale a en général un seul attribut un pointeur vers l'entrée de la table des symboles dans laquelle l'information sur l'unité lexicale est conservée. Exemple 3.1. Les unités lexicales et les valeurs d'attributs associées pour l'instruction Fortran E = M*C**2 sont données ici sous la forme de couples : Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 27 Analyse lexicale <id, pointeur vers l'entrée de la table des symboles associés à E> <op-affectation, > /* aucune valeur d'attribut n'est nécessaire, le premier composant suffit pour identifier le lexème*/. <id, pointeur vers l'entrée de la table des symboles associés à M> <op-multiplication,> <id, pointeur vers l'entrée de la table des symboles associés à C> <op-exponentiation, > <nb, valeur entière 2> /* Nous avons associé à cette unité un attribut à valeur entière. Le compilateur peut ranger la chaîne de caractères composant le nombre dans la table des symboles et faire en sorte que l'attribut de l'unité lexicale nb soit un pointeur vers la table des symboles.*/ 3.2. Mémorisation du texte d'entrée: couple de tampons On utilise un tampon divisé en deux moitiés de N caractères, comme à la figure 3.2 (N pourra être le nombre de caractères dans un bloc disque, par exemple 1024 ou 4096 Figure 3.2. Un tampon d'entrée en deux moitiés On lit N caractères d'entrées dans chacune des entrées du tampon en une seule commande système de lecture, plutôt que d'invoquer une commande de lecture pour chaque caractère d'entrée. S'il reste moins que N caractères en entrée, un caractère spécial fdf est placé dans le tampon après les caractères d'entrée. On gère deux pointeurs vers le tampon d'entrées. La chaîne de caractères entre les deux pointeurs constitue le lexème courant. Au départ les deux pointeurs désignent le premier caractère du prochain lexème à trouver. L'un appelé le pointeur « Avant », lit en avant jusqu'à trouver un modèle. Une fois que le prochain lexème est reconnu, le pointeur « Avant » est positionné sur le caractère à sa droite. Après traitement de ce lexème, les deux pointeurs sont positionnés sur le caractère qui suit immédiatement le lexème. Si le pointeur « Avant » est sur le point de dépasser la marque de moitié, la moitié droite est remplie avec N nouveaux caractères d'entrée. Si le pointeur « Avant » est sur le point de dépasser l'extrémité droite du tampon, la partie gauche est remplie avec N nouveaux caractères d'entrées, et le pointeur « Avant » poursuit circulairement au début du tampon. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 28 Analyse lexicale 3.3. Spécification des unités lexicales Les expressions régulière sont une notation importante pour spécifier (donner une expression formelle) des modèle. Chaque modèle reconnaît un ensemble de chaînes. 3.3.1. Chaînes et langage Le terme alphabet ou classe de caractère dénote tout ensemble fini de symboles. Des exemples typiques de symboles sont les lettres et les caractères. L'ensemble {0,1} est l'alphabet binaire. L' ASCII est un exemple de l'alphabet informatique. Une chaîne sur un alphabet est une séquence finie de symboles extraits de cet ensemble. La longueur d'une chaîne s, habituellement notée |s| est le nombre d'occurrences de symboles dans s. La chaînes vide notée ε, est une chaîne spéciale de longueur zéro. Voici la terminologie courante concernant des portions de chaînes (tableau 3.2). Terme Préfixe de s Suffixe de S Sous-chaîne de S Suffixe, préfixe ou souschaîne propre de S Sous suite de S Définition Une chaîne obtenue en supprimant un nombre quelconque, éventuellement nul, de symboles en fin de s; par exemple, ban est un préfixe de banane Une chaîne obtenue en supprimant un nombre quelconque, éventuellement nul, de symboles début de S; par exemple, nane est un suffixe de banane Une chaîne obtenue en supprimant un préfixe et un suffixe de S. Par exemple nan de banane. Tout suffixe ou préfixe et une sous-chaîne. Pour toute chaîne de S, S et ε sont toutes deux préfixe, suffixes et sous chaîne de S. Toute chaîne non vide se qui est, respectivement, préfixe, suffixe ou sous chaîne de s telle que s ≠ x Toute chaîne obtenue en supprimant un nombre quelconque, éventuellement nul, de symbole non nécessairement consécutifs de S; par exemple, baan est une sous suite de banane Tableau 3.2. Vocabulaire pour les portions de chaînes Le terme langage dénote un ensemble quelconque de chaînes construites sur un alphabet fixé. Si x et y sont deux chaînes, alors la concaténation de x et de y, qui s'écrit xy, est la chaîne formées en joignant x et y. Par exemple si x = porte et y = mine alors xy=portemine. La chaîne vide est l'élément neutre pour la concaténation; cela signifie que Sε = εS = S. Si l'on considère la concaténation comme un produit, on peut définir l'exponentiation de chaîne comme suit : on définit S0 comme ε et pour i > 0, on définit Si comme Si-1 S. Comme εS est S elle même, S1 = S. Donc S2 = SS ainsi de suite. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 29 Analyse lexicale 3.3.2. Opération sur les langages Plusieurs opérations importantes peuvent s'appliquer aux langages. Pour l'analyse lexicale, On s'intéresse principalement à l'union, la concaténation et la fermeture définis dans le tableau 3.3. On peut aussi généraliser l'opérateur d'exponentiation aux langages en définissant L0 comme {ε}, et Li comme Li-1 L. Ainsi, Li et L concaténé i-1 fois avec lu i même. Opération Union de L et M notée LCM Concaténation de L et M notée LM Fermeture de kleene de L notée L* Fermeture positive de L notée L+ Définition L∪M = { S | S∈L ou S ∈ M } LM = { St | S∈L et t ∈ M } L* = ∪ i∞=0 Li L* dénote un nombre quelconque éventuellement nul, de concaténation de L. L+ = ∪ i∞=1 Li L+ dénote un nombre quelconque non nul, de concaténation de L. Tableau 3.3. Opérations sur les langages Exemple 3.2. Soit L l'ensemble {A, B, …, Z, ab,…, Z} et C {0, 1, …,9}. Etant donnée qu'un symbole peut être considéré comme une chaîne de longueur 1, les ensembles L et C sont des langages finis. Voici quelques exemples de nouveaux langages crées à partir de L et C en appliquant les opérateurs définis dans le tableau 3.3 : 1. L ∪ C est l'ensemble des lettres et des chiffres. 2. LC est l'ensemble des chaînes formées d'une lettre suivie d'un chiffre. 3. L4 est l'ensemble des chaînes de quatre lettres. 4. L* est l'ensemble de toutes les chaînes de lettre y compris ε, la chaîne vide. 5. L(L∪C) est l'ensemble de toutes les chaînes de lettre et de chiffres commençant par une lettre 3.4. Reconnaissances des unités lexicales Tout au long de cette section nous utiliserons le langage engendré par la grammaire suivante comme exemple type. Exemple 3.3. instr → si expr alors instr | si expr alors instr sinon instr |ε Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 30 Analyse lexicale exp → terme oprel terme | terme terme → id | nb Où les terminaux si, alors, sinon, oprel, id et nb engendrent les ensembles de chaînes données par les définitions régulières suivantes : si → si alors → alors sinon → sinon oprel → < |<= | = | <>| > | >= id → lettre (lettre | chiffre)* nb → chiffre+(.Chiffre+)? (E(+|-)? Chiffre+)? Lettre et chiffre sont définis comme suit : lettre → A | B | … | Z | a | b | … |z chiffre → 0 | 1 | … |9 Remarque. Pour ce fragment de langage l'analyseur lexical reconnaîtra les mots clé si, alors sinon au même titre que les lexèmes dénotés par oprel, id, nb. (Pour simplifier on suppose que les mots clé sont réservés). On suppose que les lexèmes sont séparés par un espace consistant en une suite non vide de blancs, tabulations et fin de ligne. Notre analyseur lexical éliminera ces espaces en comparant une chaîne avec la définition régulière bl suivante. délim → blanc | tabulation | fin de ligne bl → délim + Notre objectif est de construire un analyseur lexical qui isole le lexème associé à la prochaine unité lexicale du tampon d'entrée et qui produise en sortie un couple composé de l'unité lexicale appropriée et d'une valeur d'attribut. Les attributs pour les opérateurs de relation (oprel) sont donnés par les constantes symboliques PPQ, PPE, EGA, DIF, PGQ, PGE. 3.4.1. Diagrammes de transition Supposons que le tampon d'entrée soit le même que dans la figure 3.2 et que le pointeur vers le début du tampon pointe sur le caractère qui suit la dernière unité lexicale trouvée. On utilise un diagramme de transition pour garder trace des informations sur les caractères rencontrés quand le pointeur « avant » parcourt le texte d'entrée. On procède par des déplacements de position en position dans le diagramme au fur et à mesure de la lecture des caractères. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 31 Analyse lexicale Exemple 3.4. - Diagramme de transition pour « si » (figure 3.3) s 0 i 1 2 autre autre Retourne Si L'étiquette autre désigne tous les caractères autres que ceux qui sont indiqués explicitement sur les arcs quittant e. Figure 3.3. Diagramme de transition pour « si » - Diagramme de transition pour « >= » (figure 3.4) Figure 3.4. Diagramme de transition pour « >= » Ce diagramme fonctionne comme suit : Dans l'état de départ 0, on lit le prochain caractère d'entrée. On suit l'arc « > » depuis 0 vers 6 si le caractère d'entrée est « > ». Sinon, on n'a réussi à reconnaître ni « > » ni « > = ». En atteignant l'état 6, on lit le prochain caractère d'entrée, l'arc étiqueté « = « entre l'état 6 et l'état 7 doit être suivi si le caractère d'entrée est « = ». Autrement, l'arc étiqueté autre conduit à l'état 8. Le double cercle sur l'état 7 indique que c'est un état d'acceptation, état dans lequel l'unité lexicale « >= » a été reconnue. Noter que le caractère « > » et un autre caractère sont lus quand on progresse dans la suite d'arcs depuis l'état initial jusqu'à l'état d'acceptation 8. Etant donné que le caractère supplémentaire ne fait pas partie de l'opérateur de relation « >= » on doit reculer le pointeur avant d'un caractère. On utilise une * pour signaler les états dans lesquels ce recul dans l'entrée doit être fait. En général, il peut y avoir plusieurs diagrammes de transition, chacun d'entre-eux spécifiant un groupe d'unités lexicales. Si un échec se produit quand on parcoure un diagramme de transition, alors on recule le pointeur avant là ou il était à l'état initial de ce diagramme et on active le diagramme de transition suivant (le pointeur avant est reculé à la position du pointeur début). Si un échec intervient dans tous les diagrammes de transition, alors une erreur lexicale a été détectée et on appelle une routine de récupération sur erreur. Exemple 3.5. La figure 3.5 représente un diagramme de transition pour l'unité lexicale oprel (opérateur de relation). Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 32 Analyse lexicale Figure 3.5. Diagramme de transition pour les opérateurs de relation Remarque. Comme les mots clés sont des suites de lettres, il y a des exceptions à la règle selon laquelle une suite de lettres ou de chiffres débutant par une lettre est un identificateur. Plutôt que de coder les exceptions dans un diagramme de transition, une astuce consiste à traiter les mots clés comme des identificateurs spéciaux. Quand on atteint l'état d'acceptation de la figure 3.6, on exécute un certain code pour déterminer si le lexème amenant à cet état d'acceptation est un mot clé ou un identificateur. Figure 3.6. Diagramme de transition pour les identificateurs et les mots clés Une technique simple pour séparer les mots clé des identificateurs consiste à diviser la table de symboles en deux parties : une partie statique au début de la table de symboles dans laquelle on place les mots clé (si, alors, sinon, etc.) avant qu'aucun caractère n'ait été lu et une partie dynamique en bas pour les identificateurs. L'instruction de retour qui suit l'état d'acceptation utilise UnilexId () et RangerId() pour obtenir l'unité lexicale et la valeur d'attribut respectivement. RangerId() a accès au tampon ou l'unité lexicale identificateur a été trouvée. On examine la table des symboles et : - Si on trouve le lexème avec l'indication mot clé, RangerId () rend 0. - Si on trouve le lexème comme variable du programme, RangerId() rend un pointeur vers l'entrée dans la table des symboles. - Si on ne trouve pas le lexème dans la table des symboles, il y est placé en tant que variable et un pointeur vers cette nouvelle entrée est retourné. La procédure UnilexId(), de manière similaire, recherche le lexème dans la table des symboles. Si le lexème est un mot clé, l'unité lexicale correspondante est retournée ; autrement, l'unité lexicale « id » est retournée. Exemple 3.6. Nombre sans signe Soit la définition régulière suivante dénotant des nombres sans signe : Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 33 Analyse lexicale nb → chiffre+ chiffres (.chiffre+)? (E(+/-)? Chiffre+) ? fractions ? exposant ? Dans cette définition, fraction et exposant sont optionnels. → Le lexème pour une unité lexicale possible doit être le plus long possible. Quand l'entrée est 12.3E4 l'analyseur ne doit pas s'arrêter après avoir vu 12 ou 12.3. Partant des états 25, 20 et 12 de la figure 3.7 on atteint les états d'acceptation après avoir vu 12, 12.3 et 12.3E4, respectivement (on suppose que 12.3E4 n'est pas suivi d'un chiffre). Figure 3.7. Diagrammes de transition pour les nombres sans signe Les diagrammes de transition avec les états de départ 25, 20 et 12 correspondent à chiffres, chiffres fraction et chiffres fraction ? exposant respectivement, on doit donc essayer les états de départ dans l'ordre inverse 12, 20, 25. Une autre solution consiste à les rassembler en un seul diagramme (figure 3.8). Figure 3.8. Diagramme de transition pour les nombres sans signe RangerNb() entre le lexème dans la table des symboles et rend l'unité lexicale « nb » avec ce pointeur comme valeur lexicale. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 34 Analyse lexicale Remarque. On peut obtenir une suite de diagrammes de transitions pour toutes les unités lexicales de l'exemple 3.3 si on réunit les diagrammes de transition étudiés. On doit essayer les états de départ de plus petit numéro avant ceux de numéros plus élevés. Le traitement de « bl » est différent : Rien n'est retrouvé quand l'état d'acceptation est atteint ; on revient simplement à l'état de départ du premier diagramme de transition pour rechercher un autre modèle. Voici un diagramme de transition qui reconnaît « bl » (figure3.9). Figure 3.9. Diagramme de transition pour « bl » Remarque. Il est préférable de rechercher d'abord les unités lexicales qui apparaissent le plus fréquemment avant celles qui apparaissent le moins fréquemment, car on atteint un diagramme de transition qu'après l'échec des diagrammes précédents. Comme des espaces peuvent apparaître fréquemment, placer le diagramme de transitions des espaces près du début peut être une amélioration par rapport au placement à la fin. 3.5. Un langage pour spécifier des analyseurs lexicaux (lex) Les analyseurs lexicaux basés sur des tables de transitions sont les plus efficaces...une fois la table de transition construite. Or, la construction de cette table est une opération longue et délicate. Le langage lex fait cette construction automatiquement : il prend en entrée un ensemble d'expressions régulières et produit en sortie le texte source d'un programme C qui, une fois compilé, est l'analyseur lexical correspondant au langage défini par les expressions régulières en question (voir figure 3.10). Figure 3.10. Utilisation de lex Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 35 Analyse lexicale En lisant cette section, souvenez-vous de ceci : lex écrit un fichier source C. Ce fichier est fait de trois sortes d'ingrédients : - des tables garnies de valeurs calculées à partir des expressions régulières fournies, - des morceaux de code C invariable, et notamment le « moteur » de l'automate, c'est-à-dire la boucle qui répète inlassablement etat Å transit (etat; caractere), - des morceaux de code C, trouvés dans le fichier source lex et recopiés tels quels, à l'endroit voulu, dans le fichier produit. Un fichier source pour lex doit avoir un nom qui se termine par « .l ». Il est fait de trois sections, délimitées par deux lignes réduites au symbole %% : %{ Déclarations pour le compilateur C %} Définitions régulières %% Règles %% Fonctions C supplémentaires La partie « déclarations pour le compilateur C » et les symboles %{ et %} qui l'encadrent peuvent être omis. Quand elle est présente, cette partie se compose de déclarations qui seront simplement recopiées au début du fichier produit. En plus d'autres choses, on trouve souvent ici une directive #include qui produit l'inclusion du fichier « .h » contenant les définitions des codes conventionnels des unités lexicales (INFEG, INF, EGAL, etc.). La troisième section « fonctions C supplémentaires » peut être absente également (le symbole %% qui la sépare de la deuxième section peut alors être omis). Cette section se compose de fonctions C qui seront simplement recopiées à la fin du fichier produit. 3.6. Conception d'un générateur d'analyseurs lexicaux Dans cette section, nous étudions la conception d'un outil logiciel qui construit automatiquement un analyseur lexical à partir d'un programme dans le langage Lex. Spécification du langage source L (définition régulière lex) Générateur automatique d'analyseurs lexicaux Analyseur lexical de L Nous supposons que nous disposons d'une spécification d'analyseur lexical de la forme : m1 {action1} Un fragment à exécuter à chaque fois qu'on rencontre dans le texte d'entrée un lexème qui concorde avec l'expression régulière mi. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 36 Analyse lexicale m2 {action2} … mn {actionn} Le but est de construire un reconnaisseur qui recherche des lexèmes dans le tampon d'entrée. Si plus d'un modèle convient, le reconnaisseur doit choisir le lexème le plus long. S'il y a au moins deux modèles qui reconnaissent le lexème le plus long, le modèle listé en premier est choisi. Un automate fini est un modèle naturel sur lequel on peut bâtir un analyseur lexical. Le compilateur lex converti les modèles d’expressions régulières de la spécification lex en une table de transition pour un automate fini. L'analyseur lexical lui-même utilise un tampon d'entrée avec deux pointeurs (début, avant). Il consiste en un simulateur d'AF qui utilise une table de transitions pour rechercher les modèles dans le tampon d'entrée (voir figure 3.11). Figure 3.11. Schéma du compilateur lex 3.6.1. Reconnaissance fondée sur les Automates finis non déterministes On commence par construire une table de transition pour un AFN N pour le modèle m1⏐m2⏐..⏐mn. On construit un AFN pour chaque mi, on ajoute un nouvel état de départ e0, et enfin on relie e0 aux états de départ de chaque N(mi) par des εtransitions. ⇒ On doit reconnaître le préfixe le plus long de la chaîne d'entrée qui correspond à un modèle. Nous apportons donc les modifications suivantes à l'algorithme 2.5 (simulation d'un AFN) - Chaque fois qu'on ajoute un état d'acceptation à l'ensemble d'états courant, on enregistre la position courante dans le texte d'entrée et le modèle mi correspondant à cet état d'acceptation. - Si l'ensemble d'états courant contient déjà un état d'acceptation, alors seul le modèle qui apparaît le premier dans la spécification lex est noté. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 37 Analyse lexicale - On continue à effectuer des transitions jusqu'à atteindre la terminaison (un ensemble d'états n'ayant pas de transition sortante sur le symbole d'entrée courant. ⇒ On recule le pointeur avant jusqu'à la position à laquelle a eu lieu la dernière concordance ⇒ - Le modèle correspondant identifie l'unité lexicale. - Le lexème est la chaîne entre le pointeur début et avant. Si aucun modèle ne marche ⇒ transfert du contrôle à une fonction de récupération sur erreur. Exemple 3.7. Supposons qu'on a le programme lex suivant : m1 a { } m2 abb { } m3 a*b+ { } /* Les actions sont omises */ Les trois unités lexicales sont reconnues par les automates de la figure 3.12 Figure 3.12. AFN reconnaissant trois modèles différents Soit la chaîne d'entrée aaba Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 38 Analyse lexicale Suite d'ensembles d'états traversés et de modèles reconnus au cours du traitement de la chaîne d'entrée aaba. Supposons que la chaîne d'entrée est la suivante : aab/ab/abbb On continue le schéma précédant en revenant à l'état de départ de l'AFN avec le quatrième caractère. 3.6.2. Reconnaissance fondée sur les Automates finis déterministes Si on convertit l'AFN de la figure 3.12 en AFD, on obtient la table de transition suivante (voir tableau 3.4): Etat Symbole d'entrée Modèle annoncé (à l'entrée de l'état) a b 0137 247 8 aucun 247 7 58 a 8 - 8 a*b + 7 7 8 aucun 58 - 68 a* b + 68 - 8 abb Tableau 3.4. Table de transition d'un AFD Remarque. La chaîne abb correspond aux deux modèles abb et a*b+, reconnus aux états 6 et 8 de l'AFN. ⇒ l'état 68 dans l'AFD inclut donc deux états d'acceptation de l'AFN puisque Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 39 Analyse lexicale abb apparaît avant a*b+ dans la spécification de l'AFN donc seul le modèle abb est reconnu à l'état 68. Sur la chaîne d'entrée aaba : m1 a 0137 b 247 a 7 m3 8 néant Jeton 1: aab, lexème a*b+ Considérons le second exemple : aba 0137 m1 a 247 b m3 58 a néant Cet état comprend l'état d'acceptation 8 du NFA donc a*b+ est reconnu et le lexème est ab Ensuite il reconnaît a : 0137 a m1 247 Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 40 Analyse syntaxique Analyse syntaxique Dans notre modèle de compilateur, l'analyseur syntaxique obtient une chaîne d'unités lexicales de l'analyseur lexical, comme illustre à la figure 4.1, et vérifie que la chaîne peut-être engendrée par la grammaire du langage source. On suppose que l'analyseur syntaxique signale chaque erreur de syntaxe, il doit également supporter les erreurs les plus communes de façon à pouvoir continuer le traitement du texte restant. Figure 4.1. Emplacement d'un analyseur syntaxique dans un modèle de compilateur Les méthodes utilisées (par l'analyse syntaxique) communément sont soit descendantes soit ascendantes. Comme leurs nom l'indique, les analyseurs descendants construisent des arbres d'analyse de haut en bas, alors que les analyseurs ascendants partent des feuilles et remontent vers le racine. Dans les deux cas, l'entrée de l'analyseur est parcourue de la gauche vers la droite, un symbole à la fois. Les méthodes descendantes ou ascendantes les plus efficaces travaillent uniquement sur des sous classes de grammaires hors contexte, mais certaines de ces sous classes sont suffisamment expressives pour décrire la plupart des constructions syntaxiques des langages de programmation. 4.1. Grammaires non contextuelles Beaucoup de constructions de langages de programmation, ont une structure récursive; ces constructions peuvent être définies par des grammaires non contextuelles. Exemple 4.1. si S1 et S2 sont des instructions et E une expression "si E alors S1 sinon S2" est une instruction Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 (1) 41 Analyse syntaxique Remarque. Cette forme d'instruction ne peut expressions régulières. être spécifiée en utilisant la notation des En utilisant les variables syntaxiques inst pour dénoter la classe des instructions et expr pour dénoter la classe des expressions, on peut exprimer (1) de façon lisible en utilisant la production: "instr→ si expr alors instr sinon instr" (2) 4.1.1. Définition d'une grammaire non contextuelle Soit une grammaire G (VT, VN, S, P) VT : ensemble des symboles terminaux de la grammaire (unités lexicales) VN : ensemble des symboles non terminaux de la grammaire (variables syntaxiques qui dénotent un ensemble de chaînes) S : l’axiome est un non terminal particulier, tel que l'ensemble des chaînes qu'il dénote est le langage défini par la grammaire. P : ensemble de règles de production de la grammaire; spécifient la manière dont les terminaux et les non terminaux peuvent être combinés pour former les chaînes. Exemple 4.2. La grammaire constituée des productions suivantes définit des expressions arithmétiques simples: expr → exp op expr expr → (expr) Dans cette grammaire les symboles terminaux sont : id + - * / ↑( ) expr → – expr les symboles non terminaux sont expr et op et l'axiome est expr op → + en abrégé: expr → id op → – op → * op → / op →↑ expr → exp op expr|(expr)|– expr| id op → +| – | * | / | ↑ 4.1.2. Définition d'une Dérivation Nous disons que αAβ ⇒ αγβ si A → γ est une production et α et β sont des chaînes arbitraires de symboles grammaticaux. si α1 ⇒ α2 ⇒ ….⇒ αn, on dit que α1 se dérive en αn. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 42 Analyse syntaxique le symbole ⇒ signifie "se dérive en une étape" pour dire "se dérive en zéro, une ou plusieurs étapes" on peut utiliser le symbole ⇒ * donc α ⇒ α pour une chaîne quelconque α et * si α ⇒ β et β ⇒ γ, alors α ⇒ γ * * + ⇒ signifie "se dérive en une ou plusieurs étapes" + soit une grammaire G et S son axiome; on peut utiliser la relation ⇒ pour définir L(G), le langage engendré par G. On dit qu'une chaîne de terminaux w appartient à + L(G) ssi S ⇒ w. La chaîne w est appelée phrase de G. un langage qui peut être engendré par une grammaire est dit langage non contextuel. si S ⇒ α, où α peut contenir certains non terminaux, on dit que α est une proto* phrase de G. Exemple 4.3. Soit la grammaire G(VT,VN,S,P) E → E+E|E*E|(E)|-E|id (3) la chaîne –(id+id) est une phrase de la grammaire (3) car on a la dérivation: E ⇒–E ⇒–(E) ⇒–(E+E) ⇒ –(id+E) ⇒ –(id+id) (4) Les chaînes E, -E, -(E),…,-(id+id) sont toutes des proto-phrases de cette grammaire. On écrit E ⇒ -(id+id) pour indiquer que E se dérive en –(id+id) * A chaque étape d'une dérivation, on doit faire deux choix : ƒ ƒ Il faut choisir le non terminal à remplacer Quelle alternative utiliser pour ce non terminal Dérivation gauche : Seul le non terminal le plus à gauche est remplacé à chaque étape. On écrit α ⇒ β on peut alors réécrire (4) g E ⇒ –E ⇒ –(E) ⇒ –(E+E) ⇒ –(id+E) ⇒ –(id+id) g g g g g (4) Chaque étape d'une dérivation gauche peut s'écrire: ωAγ ⇒ ωδγ ou ω est formé uniquement de terminaux A → δ est la production utilisée et γ est une chaîne de symboles grammaticaux. g Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 43 Analyse syntaxique α se dérive en β par une dérivation gauche est notée α ⇒ β g si S ⇒ α on dit que α est une proto-phrase gauche de la grammaire considérée. * g Dérivation droite : le terminal la plus à droite est remplacé à chaque étape. 4.2. Arbres d’analyse et dérivations Définition Arbre syntaxique: un arbre syntaxique illustre visuellement la manière dont l'axiome d'une grammaire se dérive en une chaîne du langage. Si le non terminal A est défini par la production A → XYZ, alors un arbre syntaxique peut posséder un nœud intérieur étiqueté A et avoir trois fils étiquetés X,Y et Z, de gauche à droite. A X Y Z Formellement, étant donné une grammaire non contextuelle, un arbre syntaxique est un arbre possédant les propriétés suivantes: - La racine est étiquetée par l'axiome - chaque feuille est étiquetée par une unité lexicale ou par ε - chaque nœud intérieur est étiqueté par un non terminal. Si A est le non-terminal étiquetant un nœud intérieur et si les étiquettes des fils de ce nœud sont, de gauche à droite, X1,X2,..,Xn, alors A→ X1X2…Xn est une production, ici X1,X2, …,Xn représentant soit un non terminal, soit un terminal. Un cas particulier A → ε qui signifie qu'un nœud étiqueté A a un seul fils étiqueté ε. Exemple 4.4. liste → liste + chiffre liste → liste – chiffre liste → chiffre chiffre → 0|1|2|3|4|5|6|7|8|9 liste liste liste chiffre chiffre chiffre + 5 2 9 Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 44 Analyse syntaxique Des feuilles d'un arbre syntaxique, lues de gauche à droite, constituent le mot des feuilles de l'arbre, qui est la chaîne engendrée ou dérivée à partir du non terminal situé à la racine de l'arbre syntaxique. (Dans l'exemple la chaîne engendrée est 9-5+2). 4.2.1. Ambiguïté Une grammaire peut avoir plus d'un arbre syntaxique qui engendre une chaîne donnée d'unités lexicales. Une telle grammaire est dite ambiguë. Comme une telle chaîne a habituellement plus d'une signification, on a besoin de travailler avec des grammaires non ambiguës. Exemple 4.5. chaîne → chaîne + chaîne| chaîne - chaîne|0|1|2|3|4|5-6|7|8|9 La figure 4.2 montre qu'une expression comme 9-5+2 a plus d'un arbre syntaxique. Les deux arbres pour 9-5+2 correspondent aux deux manières de parenthèser l'expression (9-5)+2 et 9-(5+2). Le deuxième parenthèsage donne à l'expression la valeur 2 plutôt que la valeur habituelle 6. chaîne chaîne chaîne _ 9 + chaîne chaîne chaîne 2 5 chaîne 9 chaîne _ chaîne 5 + chaîne 2 Figure 4.2. Deux arbres syntaxiques pour 9-5+2 Associativité des opérateurs Par convention 9+5+2 est équivalent à (9+5)+2 et 9-5 est équivalent à (9-5)-2. On dit que l'opérateur + est associatif à gauche car un opérande avec des signes + de chaque coté est traité par l'opérateur qui est à sa gauche. +,-,*,/ sont associatifs à gauche. ↑ L’exponentiation est associative à droite. := l'affectation est associative à droite. 4.2.2. Priorités des opérateurs L’expression 9+5*2 a deux interprétations (9+5)*2 ou 9+(5*2) On a besoin de connaître la priorité relative des opérateurs dès qu'il y'a plus d'un type d'opérateurs en présence. On dira que * a une priorité supérieure à celle de + si * agit sur ses opérandes avant +. Dans l'arithmétique usuelle *, / ont une priorité supérieure à +,- Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 45 Analyse syntaxique En conséquence 5 est traitée par * dans 9+5*2 et 9*5+2 ce qui est équivalent respectivement à 9+(5*2) et (9*5)+2. 4.3. Analyse descendante (TOP-DOWN) Dans cette section, nous introduisons les idées de base de l'analyse descendante qui comprend plusieurs formes (voir figure 4.3). analyse descendante sans rebroussement (Analyse prédictive) avec rebroussement (analyse descendante récursive) récursive (à la main) non récursive (génération automatique) Figure 4.3. Arborescence des méthodes d'analyse descendantes Nous définissons la classe des grammaires LL(1) à partir desquelles on peut construire automatiquement des analyseurs prédictifs. 4.3.1. Analyse par descente récursive L'analyse descendante peut-être considérée comme une tentative pour déterminer une dérivation gauche associée à une chaîne d'entrée. Elle peut-être aussi vue comme une tentative pour construire un arbre d'analyse de la chaîne d'entrée, en partant de la racine et en créant les nœuds de l'arbre suivant un ordre prédéfini. Nous présentons ici une analyse qui peut impliquer des retours arrière (nécessité de passages répétés sur la chaîne d'entrée. Exemple 4.6. Considérons la grammaire S → cAd A → ab|a et la chaîne d'entrée w = cad. On commence par construire initialement un arbre qui contient un seul nœud étiqueté S. Un pointeur d'entrée repère c, le premier symbole de w, nous utilisons la première production de S pour développer l'arbre et obtenir Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 46 Analyse syntaxique Figure 4.4.a La feuille la plus à gauche, étiquetée c, correspond au premier symbole de w. Nous avançons maintenant le pointeur d'entrée sur a, second symbole de w, et nous considérons la feuille suivante étiquetée A. Nous pouvons alors développer A en utilisant la première alternative pour A et nous obtenons l'arbre suivant: Figure 4.4.b Nous avons alors une concordance avec le second symbole d'entrée; nous avançons donc le pointeur à d (troisième symbole en entrée), et comparons d avec la feuille suivante étiquetée b. Comme b et d sont différents, nous signalons un échec et retournons à A pour voir s'il n'existe pas une autre alternative de A, non encore essayé et qui serait susceptible de produire une concordance. En retournant à A, nous devons remettre le pointeur d'entrée en position 2, la position qu'il avait quand nous sommes arrivés sur A la première fois. Nous essayons maintenant la seconde alternative de A et obtenons l'arbre de la figure suivante: Figure 4.4.c La feuille a correspond au second symbole de w et la feuille d au troisième symbole. Comme nous avons produit un arbre d'analyse pour w, nous nous arrêtons et annonçons le succès final de l'analyse. Remarque. Une grammaire récursive à gauche peut faire boucler un analyseur par descente récursive même s'il possède un mécanisme de retour arrière. En effet dans le cas de A → Aα|ab|a, en essayant de développer A, on peut éventuellement se retrouver en train de développer A de nouveau, et cela sans avoir consommé de symbole en entrée. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 47 Analyse syntaxique ⇒ Nous devons éliminer la récursivité à gauche. (Notons que le terminal d'entrée ne change que quand un terminal de la partie droite est accepté). Suppression de la récursivité gauche Définition. Une grammaire est récursive à gauche si elle contient un non-terminal A tel qu'il + existe une dérivation A ⇒ Aα, où α est une chaîne quelconque. Considérons tout d'abord le non terminal A dans les deux productions: A → Aα| β où α et β sont des suites de terminaux et de non terminaux qui ne commencent pas par A. Par exemple expr → expr + terme|terme A = expr, α=+terme et β =terme. Une application répétée de cette production fabrique une suite de α à droite de A comme la figure 3.a. Quand A est finalement remplacé par β, on obtient un β et une suite éventuellement vide de α. Figure 4.5.a On peut obtenir le même effet, en réécrivant la production définissant A de la manière suivante: A → βR R → αR|ε Ici R est un nouveau terminal. La production R→ αR est récursive à droite. Figure 4.5.b Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 48 Analyse syntaxique Exemple 4.7. expr → expr + terme |terme devient : expr → terme R R → + terme R | ε Quelque soit le nombre de A-productions, il est possible d'éliminer les récursivités à gauche immédiates par la technique suivante: Dans un premier temps, on groupe les A-productions comme suit: A → Aα1| Aα2| …| Aαm|β1|β2|….|βn où aucune βi ne commence par un A. Ensuite on remplace les A-productions par: A → β1A'|β2A'|….| βnA' A' → α1A'| α2A'| …| αm A'|ε (on suppose que αi ≠ ε) 4.3.2. Analyse prédictive non récursive On peut construire un analyseur prédictif non récursif en tenant à jour une pile. Le problème clé de l'analyse prédictive est la détermination de la production à appliquer pour développer un non terminal. L'analyseur non récursif de la figure 4.6 suivante recherche la production à appliquer dans une table d'analyse (qu'on va voir la méthode de construction ultérieurement). Figure 4.6. Modèle d'analyseur prédictif non récursif Cet analyseur possède un tampon d'entrée, une pile, une table d'analyse et un flot de sortie. • • • Le tampon d'entrée contient la chaîne à analyser, suivie de $ (marqueur fin) La pile contient une séquence de symboles grammaticaux, avec $ marquant le fond de pile. Initialement la pile contient l'axiome de la grammaire au dessus de $ La table d'analyse est un tableau à deux dimensions M [A,a], où A est un non terminal et a est un terminal ou le symbole $. L’analyseur syntaxique est contrôlé par un programme qui a le comportement suivant. Ce programme considère X, le symbole en sommet de pile et a, le symbole Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 49 Analyse syntaxique d'entrée courant, ces deux symboles déterminent l'action de l'analyseur. Il y a trois possibilités: 1. si X = a = $, l'analyseur s'arrête et annonce la réussite finale de l'analyse. 2. Si X = a ≠ $ , l'analyseur enlève X de la pile et avance son pointeur d'entrée sur le symbole suivant. 3. Si X est un non terminal, le programme consulte l'entrée M[X, a] de la table d'analyse M. Cette entrée sera soit une X-production de la grammaire, soit une erreur. Si par exemple, M[X, a] = {X→ UVW}, l'analyseur remplace X en sommet de pile par WVU (avec U au sommet).(Nous supposerons que l'analyseur se contente, pour tout résultat d'imprimer la production utilisée). si M[X, a] = erreur, l'analyseur appelle une procédure de récupération sur erreur. Le comportement de l'analyseur peut décrire en termes de ses configurations qui décrivent le contenu de sa pile et le texte d'entrée restant. Algorithme 4.1. Analyse prédictive non récursive Donnée: une chaîne w et une table d'analyse M pour une grammaire G. Résultat: Si w est dans L(G), une dérivation gauche de w, sinon une indication d'erreur. Méthode: Initialement, l'analyseur est dans une configuration dans laquelle il a $S dans sa pile avec S, l'axiome de G au sommet et w$ dans son tampon d'entrée. Le programme est le suivant: Positionner le pointeur source ps sur le premier symbole de w$ Répéter soit X le symbole en sommet de pile et a le symbole repéré par ps si X est un terminal ou $ alors si x = a alors enlever X de la pile et avancer ps sinon erreur() sinon /* X est un non terminal */ si M[X,a] = X→ Y1Y2…Yk alors début enlever X de la pile Mettre Yk,Yk-1,…,Y1 sur la pile, avec Y1 au sommet; émettre en sortie la production X→ Y1Y2…Yk fin sinon erreur() jusqu'à X = $ /* la pile est vide*/ Exemple 4.8. Considérons la grammaire suivante : E→ TE' E'→ +TE'|ε Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 50 Analyse syntaxique T→ FT' T'→ *FT'|ε F →(E)|id Voici une table d'analyse prédictive (voir tableau 4.1) pour cette grammaire. (Jusque là on n'a pas vu comment la construire) E E' T T' F id E→ TE' Symbole d'entrée * ( E→ TE' + E'→ +TE' T→ FT' F →id T'→ ε T→ FT' T'→ *FT' F →(E) ) $ E'→ ε E'→ ε T'→ ε T'→ ε Tableau 4.1. Table d’analyse Les entrées vides représentent des erreurs Les entrées non vides indiquent les productions qu'il faut utiliser pour développer le non terminal en sommet de pile. Sur la chaîne id+id*id, l'analyseur prédictif effectue la séquence d'actions de la figure 4.7. Dans la colonne symbole d'entrée, le pointeur d'entrée repère le symbole le plus à gauche de la chaîne. Pile $E $E'T $E'T'F $E'T'id $E'T' $E' $E'T+ $E'T $E'T'F $E'T'id $E'T' $E'T'F* $E'T'F $E'T'id $E'T' $E' $ Entrée id+id*id$ id+id*id$ id+id*id$ id+id*id$ +id*id$ +id*id$ +id*id$ id*id$ id*id$ id*id$ *id$ *id$ id$ id$ $ $ $ Sortie E→ TE' T→ FT' F →id T'→ ε E'→ +TE' T→ FT' F →id T'→ *FT' F →id T'→ ε E'→ ε Figure 4.7. Transitions effectuées par un analyseur prédictif sur la chaîne source id+id*id On voit que les actions de l'analyseur décrivent une dérivation gauche de la chaîne source, c'est à dire que les productions utilisées sont celles d'une dérivation gauche. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 51 Analyse syntaxique Premier et Suivant Ces fonctions nous permettent, quand c'est possible, de remplir la table d'analyse prédictive pour G. Premier (α). Si α est une chaîne de symboles grammaticaux, Premier(α) désigne l'ensemble des terminaux qui commencent les chaînes qui se dérivent de α. si α ⇒ ε, alors ε est aussi dans premier(α). * Pour calculer Premier(X), pour tout symbole de la grammaire X, appliquer les règles suivantes jusqu'à ce qu'aucun terminal ni ε ne puisse être ajouté aux ensembles Premier 1. si X est un terminal, Premier (X) est {X} 2. si X →ε est une production, ajouter ε à premier(X) 3. si X est un non terminal et X → Y1Y2…Yk une production Premier (X) = Premier (Y1) sauf ε ∪Premier (Y2) sauf ε si Y1 ⇒ ε (ε est dans premier(α)) * ∪Premier(Y3) sauf ε si Y1Y2 ⇒ ε … ∪Premier(Yk-1) sauf ε si ε ∈ (Premier(Y1) ∩ Premier(Y2) ∩ .. ∩ Premier(Yk-2) * ∪Premier(Yk) si Y1Y2..Yk-1 ⇒ ε (autrement si ε ∈ Premier (Yj) pour tout j ∈ 1,2,..k * ajouter ε à Premier (X)) Maintenant, nous pouvons calculer Premier pour une chaîne X1X2..Xn de la façon suivante: Ajouter à Premier (X1X2…Xn) tous les symboles de Premier (X1) différents de ε. Si ε est dans Premier(X1), ajouter également les symboles de premier(X2) différents de ε. Si ε est dans Premier (X1) et dans Premier(X2) ajouter également les symboles de Premier(X3) différents de ε etc. Finalement, si quel que soit i, premier (Xi) contient ε, ajouter ε à premier(X1,X2…Xn) Exemple 4.9. Soit la grammaire : E→ TE' E'→ +TE'|ε T→ FT' T'→ *FT'|ε F →(E)|id Alors : Premier (E) = Premier(T) = Premier(F) = {(,id} Premier(E') = {+,ε} Premier(T') = {*,ε} Cela s'explique par Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 52 Analyse syntaxique Premier(E) = Premier(T) sauf ε ∪[Premier(E') si ε ∈Premier(T)] Premier(T) = Premier(F) sauf ε ∪[Premier(T') si ε ∈Premier(F)] Premier(F) = Premier(( ) ∪ Premier(id) ={(,id} donc Premier(T) = Premier(F) = {(,id} car ε ∉ Premier(F) et Premier(E) = Premier(T) = {(,id} car ε ∉ Premier(T) Premier (E') = Premier(+)∪{ε} = {+,ε} Premier(T') = Premier(*)∪{ε} = {*,ε} Suivant (A). Pour chaque non-terminal A, Suivant(A) définit l'ensemble des terminaux a qui peuvent apparaître immédiatement à droite de A dans une proto-phrase, c'est à dire l'ensemble des terminaux a tels qu'il existe une dérivation de la forme S ⇒ αAaβ où α et β sont des chaînes de symboles grammaticaux. * Remarque. Il a pu exister au cours de la dérivation, des symboles entre A et a, mais, dans ce cas ils se sont dérivés en ε et ont disparu. Si A peut-être le symbole le plus à droite dans une proto-phrase, alors $ est dans Suivant (A). Pour calculer Suivant(A) pour tous les non terminaux A, appliquer les règles suivantes jusqu'à ce que aucun terminal ne puisse être ajouté aux ensembles SUIVANT. 1. Mettre $ dans Suivant(S), où S est l'axiome et $ est le marqueur droit indiquant la fin du texte source. 2. S'il y a une production A →αBβ, le contenu de Premier(β), excepté ε, est ajouté à Suivant (B) 3. S'il existe une production A →αB ou une production A →αBβ telle que Premier(β) contient ε (β ⇒ ε), les éléments de SUIVANT(A) sont ajoutés à SUIVANT(B). * Exemple 4.10. Sur la même grammaire E→ TE' E'→ +TE'|ε T→ FT' T'→ *FT'|ε F →(E)|id Suivant (E) = Suivant(E') = {),$} Suivant(T) = Suivant(T') = {+,),$} Suivant(F) = {+,*,),$} Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 53 Analyse syntaxique Explication. Suivant(E) = {$} ∪ (règle 1) (car on a F → (E) règle 2) Premier( )) = {$,)} Suivant(E') = Suivant(E) car la règle 3 s'applique sur E → TE' = {$,)} Suivant(T) = Premier(E') sauf ε règle 2 sur E' →+TE' = {+} ∪{$,)} ={+,$,)} Suivant (T') = Suivant(T) = {+,$, )} ∪ règle 3 appliquée sur E' → +TE'|ε Suivant(E') règle 3 sur T→FT' Suivant(F) = Premier(T') sauf ε règle 2 appliquée à T →FT' ={*} ∪ {+,$,)} ={*,+,$,)} ∪ suivant (T) règle 3 sur T→*FT' et T' ⇒ ε * Construction des tables d'un analyseur prédictif L’idée est la suivante: soit A→ α une production et a dans Premier(α) alors, l'analyseur développe A en α chaque fois que le symbole d'entrée courant est a. Une complication se produit quand α = ε ou α ⇒ ε. Dans ce cas, nous devons égalemet developper A en α si le symbole d'entrée courant est dans suivant(A) ou si le $ d'entrée a été atteint et si $ est dans Suivant de A. * Algorithme 4.2. Construction d'une table d'analyse prédictive Donnée: une grammaire G Résultat: une table d'analyse M pour G Méthode: 1. Pour chaque production A→ α de la grammaire, procéder aux étapes 2 et 3. 2. Pour chaque terminal a dans Premier(α), ajouter A→α à M[A,a]. 3. Si ε est dans Premier (α), ajouter A→α à M[A,b]. pour chaque terminal b dans suivant (A). 4. si ε est dans Premier (α) et $ est dans suivant(A) ajouter A →α à M[A, $]. 5. Faire de chaque entrée non définie de L une erreur Exemple 4.11. Appliquons l'algorithme 4.2 à la grammaire E→ TE' E'→ +TE'|ε Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 54 Analyse syntaxique T →FT' T'→ *FT'|ε F→ (E)|id • • • Puisque Premier(TE') = Premier(T) = {(,id}, la production E→ TE' implique que les entrées M[E,(] et M[E,id] prennent toutes les deux la valeur E→ TE' La production E' →+TE' implique que l'entrée M[E',+] prend la valeur E' → +TE'. La production E' → ε implique M[E',)] et M[E',$] prennent toutes les deux valeurs E' →ε car suivant(E') = {),$} Voici donc la table d'analyse prédictive pour la grammaire (voir tableau 4.2) E E' T T' F id E→ TE' Symbole d'entrée * ( E→ TE' + E'→ +TE' T→ FT' F →id T'→ ε T→ FT' T'→ *FT' F →(E) ) $ E'→ ε E'→ ε T'→ ε T'→ ε Tableau 4.2. Table d’analyse 4.3.3. Diagrammes de transition pour analyseurs prédictifs Pour construire, à partir d'une grammaire, le diagramme de transition d'un analyseur syntaxique prédictif, il faut d'abord éliminer les récursivités gauches de la grammaire puis la factoriser à gauche. Ensuite, pour chaque non-terminal A il faut: 1. Créer un état initial et un état final (retour); 2. Pour chaque production A Æ X1X2...Xn, créer un chemin de l'état initial à l'état final, dont les arcs sont étiquetés Xl, X2,..., Xn. L'analyseur syntaxique prédictif travaille à partir des diagrammes de transition comme suit. Il commence dans l'état initial avec l'axiome. Si, après un certain nombre d'actions, il est dans l'état s avec un arc menant à l'état t étiqueté par le terminal a, et si le prochain symbole en entrée est a, alors l'analyseur avance le curseur d'entrée d'une position sur la droite et va dans l'état t. Inversement, si l'arc est étiqueté par un non-terminal A, l'analyseur se positionne dans l'état de départ du diagramme associé à A, sans bouger le curseur d'entrée. S'il atteint l'état final de A, il va immédiatement dans l'état t, avec pour effet d'avoir « reconnu» A dans la chaîne d'entrée pendant le temps où il transitait de l'état s à l'état t. Finalement, s'il y a un arc de s à t étiqueté e, alors, depuis l'état s, l'analyseur va immédiatement dans l'état t, sans avancer son curseur d'entrée. Un programme d'analyse prédictive reposant sur un diagramme de transition essaie de faire correspondre les transitions terminales du diagramme avec les symboles de l'entrée et simule un appel récursif de procédure chaque fois qu'il doit suivre un arc étiqueté par un non-terminal. On peut obtenir une implantation non récursive en empilant l’état s chaque fois qu'il y a une transition sur un non-terminal issue de s, et Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 55 Analyse syntaxique en dépilant chaque fois que l'état final d'un non-terminal est atteint. L'implantation des diagrammes de transition sera détaillée plus loin. L'approche décrite ci-dessus est correcte à la condition que le diagramme de transition considéré ne comporte pas de non-déterminisme, c'est-à-dire s'il n'existe pas d'état ayant des transitions différentes sur le même symbole. Si une ambiguïté se produit, nous devons être capables de la résoudre d'une façon ad hoc, comme dans l'exemple suivant. Si le non-déterminisme ne peut pas être éliminé, nous ne pouvons pas construire d'analyseur syntaxique prédictif, mais, si nous pensons que c'est la meilleure stratégie d'analyse possible, nous pouvons construire un analyseur par descente récursive utilisant le rebroussement pour essayer systématiquement toutes les possibilités. Exemple 4.12. Soit la grammaire : E→ TE' E'→ +TE'|ε T →FT' T'→ *FT'|ε F→ (E)|id La figure 4.8 contient une collection de diagrammes de transition pour cette grammaire. Les seules ambiguïtés se produisent sur les transitions sur la chaîne vide. Si nous interprétons les arcs sortants de l'état initial pour E' comme signifiant de suivre la transition sur + chaque fois que c'est le prochain symbole en entrée, sinon de suivre la transition sur f, et si nous faisons une supposition analogue sur T', alors l'ambiguïté disparaît et nous pouvons écrire un programme d'analyse prédictif pour cette grammaire. Figure 4.8. Diagrammes de transition On peut simplifier les diagrammes de transition en substituant les diagrammes les uns aux autres. La figure 4.9(b) montre un diagramme de transition équivalent pour E'. Nous pouvons alors substituer le diagramme de la figure 4.9(b) pour la transition sur E' dans le diagramme pour E de la figure 4.8, ce qui produit le diagramme de la Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 56 Analyse syntaxique figure 4.9(c). Nous observons alors que le premier et le troisième nœuds de la figure 4.9(c) sont équivalents et nous les fusionnons. Le résultat obtenu à la figure 4.9( d) est illustré à nouveau comme premier diagramme de la figure 4.10. Les mêmes techniques s'appliquent aux diagrammes pour T et T'. L'ensemble complet des diagrammes résultants est montré à la figure 4.10. Une implantation en C de cet analyseur syntaxique prédictif s'exécute de 20 à 25% plus vite que l'implantation en C de la figure 4.8. Figure 4.9. Digrammes de transition simplifiés Figure 4.10. Digrammes de transition simplifiés des expressions arithmétiques 4.3.4. Grammaires LL(1) Pour certaines grammaires, la table d'analyse peut avoir des entrées qui sont définies de façon multiple. Par exemple, si G est récursive à gauche ou ambiguë, la table d'analyse aura alors au moins une de ses entrées défini de façon multiple. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 57 Analyse syntaxique Exemple 4..13. Soit la grammaire G S → iEtSS'|a S' → eS|ε E→b Voici la table d'analyse pour cette grammaire. S S' • • • a S →a b E→b E Symbole d'entrée e i S→ iEtSS' S' → ε S' → eS t $ S'→ε L'entrée M[S',e] contient à la fois S' → eS et S' → ε, car Suivant(S') = {e,$} (donc la règle 3, si ε est dans Premier(α), ajouter A →α à M[A,b] pour chaque terminal b dans Suivant((A)) D'autre part, premier(α) = {e} donc mettre S' → eS dans M[S',e] La grammaire est ambiguë (choix de la production à utiliser quand on voit e (sinon). Nous pouvons résoudre cette ambiguïté en choisissant S' →eS Une grammaire dont la table d'analyse n'a aucune entrée définie de façon multiple est appelée LL(1). Le premier "L" de LL(1) signifie "Parcours de l'entrée de la gauche vers la droite" (left to right scanning of the input), le second "L" signifie "Dérivation gauche" (Leftmost Dérivation) et le "1" indique qu'on utilise un seul symbole d'entrée de prévision à chaque étape nécessitant la prise d'une décision d'action d'analyse. Les grammaire LL(1) ont un certain nombre de propriétés distinctives. • • Aucune grammaire ambiguë ou récursive à gauche ne peut être LL(1). Une grammaire est LL(1) si et seulement si, chaque fois que A →α |β sont deux productions distinctes de G, les conditions suivantes s'appliquent: 1. Pour aucun terminal a, α et β ne se dérivent toutes les deux en des chaînes commençant par a. 2. Une des chaînes au plus α et β peut se dériver en la chaîne vide. 3. Si β ⇒ ε, α ne se dérive pas en une chaîne commençant par un terminal de Suivant(A). Cela causerait une entrée multiple (voir exemple précédent) car l'un des premiers de α serait égale à un suivant de A. * On peut alors vérifier que la grammaire E→ TE' E'→ +TE'|ε T →FT' T'→ *FT'|ε Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 58 Analyse syntaxique F→ (E)|id est LL(1) Les grammaires S → iEtS|iEtSeS|a E →b (1) S → iEtSS'|a S' → eS| ε E→b (2) ne sont pas LL(1) La grammaire (1) n'est pas LL(1) à cause de la règle (1) La grammaire (2) n'est pas LL(1) à cause des productions S' → eS| ε qui est sous la forme A →α |β, avec β ⇒ ε mais α=eS se dérive en une chaîne commençant par e ∈ Suivant S' = {e,$}. * Remarque. Qu'est ce qu'on doit faire lorsque la table d'analyse a des entrées à valeurs multiples? Réponse: Transformer la grammaire afin d'éliminer les récursivités à gauche puis factoriser à gauche, Mais c'est pas encore sur d'obtenir une table d'analyse dans entrées multiples. La grammaire S→ iEtSS'|a S' →eS|ε E →b est un exemple de grammaires n'ayant pas de transformations la rendant LL(1). On peut cependant l'analyser en supposant que M[S',e] = {S'→eS}. De manière générale, il n'existe pas de règles universelles par lesquelles une entrée à valeur multiple peut être transformée en une entrée à valeur simple sans affecter le langage reconnu par l'analyseur. 4.4. Analyse ascendante (décalage/réduction) Dans cette section, nous introduisons un modèle général d'analyse syntaxique ascendante, connu sous le nom d'analyse par décalage-réduction. On va consacrer la section suivante à une forme d'analyse par décalage-réduction facile à implémenter, appelée analyse par précédence d'opérateurs. Une méthode beaucoup plus générale, appelée analyse LR, est présentée à la section qui suit. Elle est utilisée dans un grand nombre de constructeurs automatiques d'analyseurs syntaxiques. L'analyse par décalage-réduction a pour but de construire un arbre d'analyse pour une chaîne source en commençant par les feuilles (le bas) et en remontant vers la racine (le haut). Ce processus peut-être considéré comme la "réduction" d'une chaîne w vers l'axiome de la grammaire. A chaque étape de réduction, une sous-chaîne particulière correspondant à la partie droite d'une production est remplacée par le Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 59 Analyse syntaxique symbole de la partie gauche de cette production. Si la sous chaîne est choisie correctement à chaque étape, une dérivation droite est ainsi élaborée en sens inverse. Exemple 4.14. Soit la grammaire: S → aABe A→ Abc|b B →d (1) La phrase abbcde peut être réduite vers S par les étapes suivantes: abbcde aAbcde aAde aABe S production utilisée A→ b production utilisée A→ Abc production utilisée B→ d production utilisée S → aABe On parcourt abbcde à la recherche d'une sous-chaîne qui corresponde à la partie droite d'une production. Les sous-chaînes b et d conviennent. choisissons la sous chaîne b la plus à gauche (c'est pas une règle) et remplaçons la par A. Maintenant les sous-chaînes Abc,b et d correspondent aux parties droites de production. On va choisir la production A → Abc malgré que b est la sous chaîne la plus à gauche qui corresponde à une partie droite de production (on va voir ultérieurement les règles de choix entre les chaînes). Nous obtenons aAde. Remplaçons d par d par B (B→d) nous obtenons aABe. Nous pouvons maintenant remplacer cette chaîne tout toute entière par S. Nous avons donc par une séquence de quatre réductions été capables de réduire abbcde vers S. Ces réductions, élaborent en sens inverse la dérivation droite suivante: S ⇒ aABe ⇒ aAde ⇒ aAbcde ⇒ abbcde d d d d 4.4.1. Définition d'un Manche De façon informelle, un "manche" de chaîne est une sous chaîne qui correspond à la partie droite d'une production et dont la réduction vers le non terminal de la partie gauche de cette production représente une étape le long de la dérivation droite inverse. Formellement: un manche de proto-phrase droite γ est: - Une production A→β Une position dans γ où β peut être trouvée et remplacée par A pour produire la proto-phrase droite précédente dans une dérivation droite de γ. C'est à dire que si S ⇒ αAω ⇒ αβω, A→β dans la position suivant (α) est une manche * de αβω, la chaîne ω à droite du manche, ne contient que des terminaux. d d Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 60 Analyse syntaxique Remarque. Dans l'exemple 1. Si nous remplaçons b par A dans la seconde chaîne aAbcde nous obtenons la chaîne aAAcde qui par la suite ne peut être réduite vers S donc A→b dans la position 3 n'est pas un manche. Dans l'exemple 1, abbcde est une proto-phrase droite dont le manche est A →b en position 2. De même aAbcde est une proto-phrase droite dont le manche est A→Abc en position2. Exemple 4.15. Soit G: (1) E →E+E (2) E →E *E (3) E →(E) (4) E →id et la dérivation droite: (2) E ⇒ E+E d ⇒ E+E*E d ⇒ E+E*id3 d ⇒ E+id2*id3 d ⇒ id1+id2*id3 d On a indicé les id pour faciliter la discussion; nous avons aussi souligné un manche de chaque proto-phrase droite. Par exemple id1 est un manche de la proto-phrase droite id1+id2*id3 car id est la partie droite de la production E → id et le remplacement de id1 par E produit la proto-phrase droite précédente E +id2*id3. Remarque. La chaîne apparaissant à droite d'un manche contient uniquement des symboles terminaux. Puisque il existe une autre dérivation droite pour la même chaîne. E ⇒ E*E d ⇒ E*id3 d ⇒ E+E * id3 d ⇒ E + id2*id3 d ⇒ id1+id2*id3 d La grammaire 2 est ambiguë Considérons la proto-phrase droite E+E*id3. Dans cette dernière dérivation, E+E est un manche de E+E*id3 alors que, selon la première dérivation id3 est un manche de cette même proto-phrase droite. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 61 Analyse syntaxique 4.4.2. Elagage du manche La réduction d'un manche par la partie gauche de la production correspondante est « l'élagage du manche ». Une dérivation droite inverse peut être obtenue par « élagage du manche ». Plus précisément, nous commençons avec une chaîne de terminaux ω que nous désirons analyser. Si ω est une phrase de la grammaire considérée alors ω = γn où γn est la nième proto-phrase droite d'une dérivation droite encore inconnue. S = γ0 ⇒ γ1 ⇒ γ2 ⇒ ..… ⇒ γn-1 ⇒ γn = ω d d d d d Pour reconstruire cette dérivation en ordre inverse, on repère le manche βn dans γn et on remplace βn par la partie gauche d'une production An→βn pour obtenir la (n-1)ème proto-phrase droite γn-1. Nous poursuivons le même travail pour obtenir γn-2 ensuite après un nombre d'étapes on obtient une proto-phrase droite formée uniquement par S. Nous arrêtons et annonçons la réussite finale de l'analyse. L'inverse de la séquence des productions utilisées dans les réductions est une dérivation droite de la chaîne d'entrée. Exemple 4.16. E →E+E E →E*E E →(E) E →id et la chaîne d'entrée: id1+id2*id3 (2) Le tableau 4.3 présente une séquence de réductions qui réduit id1+id2*id3 vers l'axiome E. Remarque. On observe que la séquence de proto-phrases droites est exactement l'inverse de la première dérivation droite de la grammaire 2. Proto-phrase Droite id1+id2*id3 E+id2*id3 E+E*id3 E+E*E E+E E Manche id1 id2 id3 E*E E+E Production de réduction E→id E →id E→id E→E*E E→E+E Tableau 4.3. Réductions effectuées par un analyseur par décalage-réduction 4.4.3. Implantation à l'aide d'une pile de l'analyse par décalage-réduction Une bonne façon est d'utiliser une pile pour conserver les symboles grammaticaux et un Tampon d'entrée pour contenir la chaîne ω à analyser. Nous utilisons le symbole $ pour marquer à la fois le fond de pile et l'extrémité droite du tampon d'entrée. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 62 Analyse syntaxique Initialement la pile est vide et la chaîne ω est dans le tampon d'entrée. PILE $ ENTREE ω$ L'analyseur opère en décalant zéro, un ou plusieurs symboles d'entrée du tampon vers la pile jusqu'à ce qu'un manche β se trouve en sommet de pile. L'analyseur réduit alors β vers la partie gauche de la production appropriée. L'analyseur recommence ce cycle jusqu'à ce qu'il détecte une erreur ou jusqu'à ce que la pile contienne l'axiome et que le tampon d'entrée soit vide PILE $S ENTREE $ S'il atteint cette configuration, l'analyseur s'arrête et annonce la réussite finale de l'analyse. Exemple 4.17. La table suivante montre les actions que doit effectuer un analyseur par décalageréduction sur la chaîne id+id2*id3 sur la grammaire: E→ E+E |E*E| (E) | id en utilisant la première dérivation: E ⇒ E+E ⇒ E+E*E ⇒ E+E*id3→E+id2*id3 ⇒ id1 + d d d d id2 * id3 Pile (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) $ $id1 $E $E+ $E+id2 $E+E $E+E* $E+E*id3 $E+E*E $ E+E $E Entrée id1+id2*id3 $ +id2*id3 $ +id2*id3 $ *id3 $ *id3 $ id3 $ $ $ $ $ $ Action décaler réduire par E→ id décaler décaler réduire par E→id décaler décaler réduire par E →id réduire par E →E*E réduire par E →E+E accepter Remarque. Du fait que la grammaire permet deux dérivations droites pour cette entrée. Il existe une autre séquence d'actions qu'un analyseur par décalage-réduction pourrait effectuer Préfixes viables. Les préfixes d'une proto-phrase droite qui peuvent apparaître sur la pile d'un analyseur par décalage réduction sont appelés préfixes viables. 4.4.4. Analyseurs LR Cette section présente une technique efficace d'analyse syntaxique ascendante qui peut être utilisée pour analyser une large classe de grammaires non contextuelles. Cette technique est appelée analyse LR(K); "L" signifie parcours de l'entrée de gauche Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 63 Analyse syntaxique vers la droite" (left to right scanning of the input), "R" signifie "en construisant une dérivation droite inverse) et K indique le nombre de symboles de prévision utilisés pour prendre les décision s d'analyse. Quand (K) est omis, K est supposé être égal à un. Après avoir présenté le fonctionnement d'un analyseur LR, nous présenterons trois techniques pour construire les tables d'analyse LR pour une grammaire. - La première méthode appelée "simple LR" (SLR en abrégé), est la plus facile à implémenter, mais la moins puissante des trois. Pour certaines grammaires, la production des tables d'analyse peut échouer alors qu'elle réussirait avec d'autres méthodes. - La seconde méthode, appelée LR canonique, est la plus puissante, mais elle est la plus coûteuse. - la troisième méthode appelée "LookAheadLR" (LALR en abrégé) ou LR à prévision, a une puissance et un coût intermédiaires entre les deux autres. Algorithme d'analyse LR Figure 4.11. Modèle d'un analyseur LR La figure 4.11 montre qu'un modèle d'analyseur LR consiste en : un tampon d'entrée, un flot de sortie, une pile, un programme pilote et des tables d'analyse subdivisées en deux parties (Action et Successeur). Le programme moteur est le même pour tous les analyseurs LR; seules les tables d'analyse changent d'un analyseur à l'autre. Le programme d'analyse lit les unités lexicales l'une après l'autre dans le tampon d'entrée, il utilise une pile pour y ranger les chaînes de la forme S0X1S1X2S2…XmSm, où Sm est au sommet. chaque Xi est un symbole de la grammaire et chaque Si est un symbole appelé état. La combinaison du numéro de l'état en sommet de pile et du symbole d'entrée courant est utilisée pour indicer les tables et déterminer l'action d'analyse "décaler ou réduire" à effectuer. Les tables d'analyse contiennent deux parties, une fonction d'actions Action et une fonction de transfert; Successeur. d'analyse; Le programme dirigeant l'analyseur LR se comporte de la façon suivante: - Il détermine Sm, l'état en sommet de la pile, et ai, le symbole terminal d'entrée courant. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 64 Analyse syntaxique - Il consulte Action [Sm,ai] ( l'entrée de la table des actions pour l'état Sm et le terminal ai) qui peut avoir l'une des quatre valeurs: 1. décaler S, où S est un état. 2. réduire par une production de la grammaire A →β 3. accepter et 4. erreur. - La fonction successeur prend comme argument un état et un symbole non terminal et retourne un état. Une configuration d'un analyseur LR est un couple dont le premier composant est le contenu de la pile et le second est la chaîne d'entrée restant à analyser. (S0X1S1 X2 S2 ……..XmSm, ai ai+1… an $) Cette configuration représente la proto-phrase droite: X1 X2…Xm ai ai+1…an L'action suivante de l'analyseur est déterminée par la lecture de ai , le symbole d'entrée courant, Sm, l'état en sommet de pile, et la consultation de l'entrée Action[Sm,ai] de la table des actions d'analyse. Les configurations résultantes, après chacun des quatres types d'actions sont les suivantes: 1. si Action [Sm,ai] = décaler S, l'analyseur exécute une action décaler, atteignant la configuration: (S0X1S1X2S2….XmSm ai S, ai+1…an$) Ici l'analyseur a à la fois epilé le symbole d'entrée courant ai et le prochain état S, qui est donné par Action[Sm,ai], ai+1 devient le symbole d'entée courant. 2. si Action [Sm, ai] = réduire par A → β, alors l'analyseur exécute une action réduire, atteignant la configuration: (S0X1S1X2S2….Xm-rSm-r A S, ai ai+1…an$) où S = Successeur[Sm-r,A], production.. r = la longeur de β, partie droite de la Ici l'analyseur commence par dépiler 2r symboles ( r symboles d'états et r symboles grammaticaux), exposant ainsi l'état Sm-r au sommet. L'analyseur empile alors à la fois A, partie gauche de la production et S, l'entrée pour successeur [Sm-r,A]. Nous supposerons que la sortie est formée de la liste des productions par lesquelles on réduit. 3. si Action[Sm, ai] = accepter, l'analyse est terminée. 4. si Action [Sm, ai] = erreur, l'analyseur a découvert une erreur et appelle une routine de récupération sur erreur. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 65 Analyse syntaxique Algorithme 4.3. Analyse LR Donnée : Une chaîne d'entrée w et des tables d'analyse LR (les fonctions Action et successeur) pour une grammaire G. Résultat : Si w est dans L(G), une analyse ascendante de w, sinon une indication d'erreur. Méthode : Initialement, l'analyseur a S0 en pile, où S0 est l'état initial et w$ dans son tampon d'entrée. Initialiser le pointeur ps sur le premier symbole de w$ répéter indéfiniment début soit S l'état en sommet de pile et a le symbole pointé par ps; si Action [s,a] = décaler S' alors début empiler a puis S' avancer ps sur le prochain symbole d'entrée fin sinon si Action[s,a] = réduire par A →β alors début dépiler 2*|β| symboles; soit S' le nouvel état sommet de pile; empiler A puis successeur [S',A]; emettre en sortie une identification de production A →β fin sinon si Action [S,a] = accepter alors retourner sinon Erreur () Fin Exemple 4.18. La table 4.4 présente les fonctions Action et Successeur des tables d'analyse LR pour la grammaire des expressions arithmétiques restreintes aux opérateurs binaires + et *. (1) (2) (3) (4) (5) (6) E →E+T E →T T →T * F T →F F → (E) F →id (5) Le codage des actions est : 1. di signifie décaler et empiler l'état i 2. rj signifie réduire par la production (j) 3. acc signifie accepter et 4. une case vide signifie une erreur Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 66 Analyse syntaxique Action id 0 1 2 3 4 5 6 7 8 9 10 11 + * ( d5 ) $ r2 r4 Acc r2 r4 d4 d6 r2 r4 d7 r4 d5 d4 r6 r6 r6 d5 d5 d11 r1 r3 r5 d7 r3 r5 F 3 8 2 3 9 3 10 r6 d4 d4 d6 r1 r3 r5 Successeur E T 1 2 r1 r3 r5 Tableau 4.4. Table d'analyse pour la grammaire des expressions Notons que l'état atteint par transition sur le symbole a depuis l'état s est identifié dans le champs Action[S,a] en même temps que l'action décaler, et que l'état atteint par transition sur le non terminal A depuis l'état S se trouve en successeur [S,A]. Sur le texte d'entrée id*id+id, la séquence des contenus de la pile et du tampon d'entrée est présenté à la figure 4.12: PILE ENTREE ACTION (1) 0 id * id + id $ décaler d5 (2) 0 id 5 * id + id $ (3) 0 F 3 * id + id $ réduire par T →F (r4) (4) 0 T 2 * id + id $ décaler 7 (5) 0 T 2 * 7 id + id $ décaler 5 (6) 0 T 2 * 7 id 5 + id $ (7) 0 T 2 * 7 F 10 + id $ (8) 0 T 2 + id $ réduire par E → T (9) 0 E 1 + id $ décaler 6 (10) 0 E 1 + 6 id $ décaler 5 (11) 0 E 1 + 6 id 5 $ (12) 0 E 1 + 6 F 3 $ (13) 0 E 1 + 6 T 9 $ réduire par E → E+T (14) 0 E 1 $ accepter réduire par F → id (r6) réduire par F → id (r6) réduire par F → id réduire par F → id (r6) réduire par T → F (r4) Figure 4.12. Transitions d’un analyseur LR sur id*id+id Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 67 Analyse syntaxique A la ligne (1) l'analyseur LR est dans l'état 0 avec id comme premier symbole en entrée. L'entrée ligne 0 colonne id de la table Action de la figure 4 est d5 qui signifie décaler et empiler l'état 5. C'est ce qui a été fait à la ligne (2). A ce moment, * devient le symbole d'entrée courant et l'action dans l'état 5 sur l'entrée * est réduire par F → id. Deux symboles sont dépilés, (un symbole d'état et un symbole de la grammaire). L’état 0 est donc au sommet de la pile. Comme la valeur du champ successeur pour l'état 0 sur F est 3, F et 3 sont empilés. Grammaires LR Une grammaire pour laquelle nous pouvons construire des tables d'analyse est appelée grammaire LR. Intuitivement, pour qu'une grammaire soit LR, il suffit qu'un analyseur par décalageréduction gauche-droite soit capable de reconnaître les manches quand ils apparaissent en sommet de pile. Il existe une différence significative entre les grammaires LL et LR. Pour qu'une grammaire soit LR(k), on doit être capable de reconnaître l'occurrence de la partie droite d'une production en ayant vu tout ce qui est dérivé de cette partie droite et une prévision de k symboles en entrée. Cette condition est beaucoup moins contraignante que pour les grammaires LL(k), pour lesquelles on doit être capable de reconnaître l'usage d'une production à la vue des k premiers symboles de dérivés de sa partie droite. Remarques. 1. Le symbole d'état en sommet de pile contient toutes les informations dont l'analyseur LR a besoin pour savoir quand un manche apparaît au sommet. 2. S'il est possible de reconnaître un manche en connaissant uniquement les symboles grammaticaux en pile, il existe un automate à états fini qui peut, en lisant les symboles grammaticaux de la pile depuis le fond vers le sommet, déterminer quel manche, s'il y'en a, est en sommet de pile. Les Fonctions Action et Successeur des tables d'analyse LR représentent essentiellement la fonction de transition d'un automate fini. 3. L'automate fini n'a cependant pas besoin de lire la pile à chaque transition. Le symbole d'état stocké en sommet de pile est l'état dans lequel l'automate fini reconnaissant les manches serait s'il avait lu depuis le fond vers le sommet , les symboles grammaticaux de la pile . L'analyseur LR peut donc déterminer à partie de l'état en sommet de pile, toutes les informations de la pile qu'il a besoin de connaître. 4.4.5. Construction des tables d'analyse SLR à partir d'une grammaire Nous donnerons trois méthodes qui diffèrent par leur puissance et leur facilité d'implémentation. La première appelée "Simple LR" ou SLR en abrégé, est la moins puissante des trois en termes du nombre de grammaires pour lesquelles elle réussit mais elle est la plus simple à implémenter. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 68 Analyse syntaxique Des tables d'analyse Construites par cette méthode sont appelées Tables SLR et l'analyseur est appelé analyseur SLR. Une grammaire pour laquelle il est possible de construire un analyseur SLR est appelée grammaire SLR. Définition. Un item LR(0) (item en abrégé) d'une grammaire G est une production de G avec un point repérant une position de sa partie droite. Exemple 4..19. A → XYZ ⇒ A → •XYZ A → X•YZ A → XY•Z A → XYZ• La production A → ε fournit uniquement l'item A → • Intuitivement, un item indique la « quantité » de partie droite qui a été reconnue, à un moment donné, au cours du processus d'analyse. Exemple 4.20. A → XYZ indique qu'on espère avoir en entrée une chaîne dérivable depuis XYZ A → X•YZ indique que nous venons de voir en entrée une chaîne dérivée de X et que nous espérons maintenant voir une chaîne dérivée de YZ. Exemple 4.21. (1) E → E+T ⇒ E → •E+T, E → E•+T, E → E+•T, E → E+T• (manche reconnu) (2) E → T ⇒ E → •T , E → T• (manche reconnu) (3) T→ T * F ⇒ T→ •T * F, T→ T• * F, T→ T *• F, T→ T * F• (manche reconnu) (4) T → F ⇒ T →• F , T → F• (manche reconnu) (5) F → (E) ⇒ F → • (E) , F → (•E) , F → (E•) , F → (E) • (manche reconnu) (6) F → id ⇒ F → • id , F → id• (manche reconnu) L'idée centrale de la méthode SLR est de construire, tout d'abord, à partir de la grammaire, un automate fini déterministe pour reconnaître les préfixes viables. - Les items sont regroupés en ensembles qui constituent les états de l'analyseur SLR - Les items peuvent être vus comme les états d'un NFA reconnaissant les préfixes viables, et le "regroupement" est exactement la construction des sous ensembles présentée lors de la conversion NFA → DFA. - une collection d'ensembles d'items LR(0), que nous appellerons collection LR(0) canonique fournit la base de la construction des analyseurs SLR. - Pour construire la collection LR(0) canonique pour une grammaire, nous définissons une grammaire augmentée et deux fonctions Fermeture et Transition. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 69 Analyse syntaxique Définition: Si G est une grammaire d'axiome S, alors G', la grammaire augmentée de G, est G avec un nouvel axiome S' et une nouvelle production S'→ S. But: indiquer à l'analyseur quand il doit arrêter l'analyse et annoncer que la chaîne d'entrée a été acceptée. L'opération fermeture. Si I est un ensemble d'items pour une grammaire G, Fermeture de I est l'ensemble d'items construit à partir de I par les deux règles: 1. Initialement, placer chaque item de I dans Fermeture (I) 2. Si A → α•Bβ est dans fermeture (I), et B → γ est une production, ajouter l'item B→•γ à Fermeture (I), s'il ne s'y trouve pas déjà. Nous appliquons cette règle jusqu'à ce qu'aucun nouvel item ne puisse plus être ajouté à Fermeture de I. Intuitivement, si A → α•Bβ est dans Fermeture (I) cela indique que à un certain point du processus d'analyse, nous pourrions voir se présenter dans l'entrée, une sous-chaîne dérivable depuis Bβ comme entrée. Si B → γ est une production, nous supposons que nous pourrions également voir, à ce moment là une chaîne dérivable depuis γ. Autrement : soit A → α•Bβ avec B un non terminal, espérer avoir une chaîne dérivable depuis B c'est espérer avoir toute chaîne dérivable à partir de toute partie droite B → γ. Exemple 4.22. Considérons la grammaire augmentée des expressions: E' → E E → E+T |T T → T*F|F F → (E) |id (5) Si I est l'ensemble formé de l'unique item {[E'→ • E]}, alors Fermeture (I) contient les items: E' → • • E+T •T on a donc E' → •E E → •E+T E → •T T → •T*F Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 • T*F •F • (E) • id ici E' → •E est placé dans Fermeture (I) par la règle (1). Comme il y'a un E immédiatement à droite d'un 70 Analyse syntaxique T → •F F → • (E) F → • id point, par la règle (2) nous ajoutons les E-productions avec des points aux extrémités gauches. C'est –à dire E→•E+T et E → •T, nous avons maintenant un T immédiatement à droite d'un point, nous ajoutons donc T → •T*F et T → •F puis le F à droite d'un point implique l'ajout de F→•(E) et F → •id. On ne peut plus alors ajouter aucun autre item à Fermeture (I) par la règle (2). L'opération Transition. Transition (I,X) où I est un ensemble d'items et X est un symbole de la grammaire. Transition(I,X) est définie comme la fermeture de l'ensemble de tous les items [A→αX•β] tel que [[A→α•Xβ] appartienne I. Intuitivement, si I est l'ensemble d'items qui sont valides pour un préfixe viable donné γ, alors Transition(I,X) est l'ensemble des items qui sont valides pour le préfixe viable γX. Exemple 4.23. Si I est l'ensemble des deux items {[E'→E•], [E→ E•+T], alors Transition(I,+) consiste en: E → E+•T T → •T*F T → •F F → • (E) F → • id Nous calculons Transition(I,+) en recherchant dans I, les items ayant + immédiatement à droite du point, E'→ E• ne convient pas, mais E →E•+T répond au critère. Nous faisons franchir au point + afin d'obtenir {[E→ E+•T]}, puis nous calculons Fermeture sur cet ensemble. Algorithme 4.4. Construction des ensembles d'items Voici l'algorithme pour construire C, la collection canonique d'ensembles d'items LR(0) pour la grammaire augmentée G'. Procedure Items (G'); début C := {fermeture ({[S' → •S]})} répéter pour chaque ensemble d'items I de C et pour chaque symbole de la grammaire X tel que Transition(I,X) soit non vide et non encore dans C ajouter Transition (I,X) à C fin pour jusqu'à ce qu'aucun nouvel ensemble d'items ne puisse être plus ajouté à C Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 71 Analyse syntaxique fin Exemple 4.24. La collection canonique d'ensembles d'items LR(0) pour la grammaire augmentée : G' (V, ∑, R, S), avec V = {E', E, T, F, +,*,(,), id}, ∑= {+,*,(,),id}, S = E' et R définit par les règles de production suivantes: E' → E E → E+T |T T → T*F|F F → (E) |id (5) est présentée ci dessous: I0 : {fermeture ({[E'→ •E]})} E' → •E E → •E+T E → •T T → •T*F T → •F F → • (E) F → • id I1 := transition(I0,E) E' → E• E → E•+T I7 := Transition (I2, *) T → T*•F F → • (E) F → • id I8 := Transition (I4, E) F → (E•) E → E•+T I9 := transition (I6,T) E → E+T• T → T•*F I2 := transition(I0,T) Transition (I4,F) = I3 Transition (I4, ( ) = I5 I3 := Transition (I0,F ) T → F• I10 = Transition (I7, F) I4 := Transition (I0, ( ) I11 = Transition (I8, ) ) F → (E)• E → T• T → T•*F F → (•E) E → •E+T E → •T T → •T*F T → •F F → • (E) F → • id T → T*F• Transition (I0,+) = ∅ Transition (I0,*) =∅ Transition (I0, ) ) = ∅ I5 := Transition (I0, id ) F → id• I6 := transition (I1,+) Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 72 Analyse syntaxique E → E+•T T → •T*F T → •F F → • (E) F → • id La fonction de Transition pour cet ensemble est donnée sous la forme d'un diagramme de transition d'un automate fini déterministe D (voir figure 4.13). Figure 4.13. Diagramme de transition de l'automate fini déterministe D reconnaissant les préfixes viables. Remarque 1. Si chaque état D de la figure 6 est un état final et I0 est l'état initial, alors D reconnaît exactement les préfixes viables de la grammaire G' de l'exemple 10. Remarque 2 : On peut imaginer un NFA N dont les états sont les items eux-mêmes avec une transition de [A→α•Xβ] vers [A→αX•β] étiquetée X, et il y'a une transition de [A→α•Bβ] vers B→γ étiquetée ε. Alors Fermeture(I) pour l'ensemble d'items (états de N) I est exactement la ε-fermeture de l'ensemble des états du NFA déjà vue dans le cours. Donc Transition (I,X) donne la transition depuis I sur le symbole X dans le DFA produit à partir de N par la construction des sous ensembles. Items Valides. Nous disons que l'item [A →β1•β2] est valide pour un préfixe viable αβ1 s'il existe une dérivation S ' ⇒ αAω ⇒ αβ1β 2ω ( w ∈ Σ*) d d * Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 73 Analyse syntaxique En général, un item sera valide pour plusieurs préfixes viables. Exemple 4.25. Soit le préfixe viable E+T* (αβ1); Cherchons les items valides pour ce préfixe. Cherchons alors toutes les possibilités de dérivation qui font apparaître ce préfixe. E ⇒E+T E ⇒E+T*F donc T→T* • F est un item valide pour E+T* β1 suffixe du préfixe viable β2 Continuons E⇒ E+T ⇒ E+T*F ⇒ E+T*(E) donc F →•(E) β2 aussi E⇒ E+T ⇒ E+T*F ⇒ E+T*id donc F →•id β2 β1 vide Nous pouvons facilement calculer l'ensemble des items valides pour chaque préfixe viable qui peut apparaître sur la pile d'un analyseur LR, en utilisant le théorème suivant: Théorème: L'ensemble des items valides pour le préfixe viable γ est exactement l'ensemble des items atteints depuis l'état initial, le long d'un chemin étiqueté γ dans le DFA construit à partir de la collection canonique d'ensembles d'items dont les transitions sont données par transition. Remarque. le fait que A →β1•β2 soit valide pour αβ1 nous en dit beaucoup sur l'action décaler ou réduire que l'on doit effectuer quand on trouve αβ1 sur la pile d'analyse. • • si β2 ≠ ε, il suggère que nous n'avons pas encore décalé le manche sur la pile donc on doit décaler si β2 = ε, il semblerait que A→ β1 soit le manche et que nous devions réduire par cette production. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 74 Analyse syntaxique Tables d'analyse SLR On va voir comment construire les fonctions Action et Successeur à partir du DFA qui reconnaît les préfixes viables. Algorithme 4.5. Construction des Tables d'analyse SLR Donnée : Une grammaire augmentée G' Résultat : Les tables d'analyse SLR des fonctions Action et Successeur pour G' Méthode : 1. Construire C = {I0,I1,…,In}, la collection des ensembles d'items LR(0) 2. L'état i est construit à partir de Ii. Les actions d'analyse pour l'état i sont déterminés comme suit: a) si [A →α•aβ ] est dans Ii et Transition (Ii,a) = Ij, remplir Action[i,a] avec "décaler j". Ici a doit être un terminal. b) si [A →α•] est dans Ii, remplir Action [i,a] avec "réduire par A →α" pour tous les a de suivant (A); ici A ne doit pas être S'. c) si [S' →S•] est dans Ii, remplir Action[i,$] avec "accepter" si les règles précédentes engendrent des actions conflictuelles, nous disons que la grammaire n'est pas SLR(1). Dans ce cas, l'algorithme échoue et ne produit pas d'analyseur. 3. On construit les transitions successeur pour l'état i pour tout non terminal A en utilisant la règle: si Transition (Ii,A) = Ij, alors Successeur [i,A] = j 4. Toutes les entrées non définies par les règles (2) et (3) sont positionnées à "erreur" 5. L'état initial de l'analyseur est celui qui est construit à partir de l'ensemble d'items contenant [S' →•S]. Les tables d'analyse formées des fonctions Action et successeur déterminées par l'algorithme 3 sont appelées tables SLR(1) pour G. Un analyseur LR utilisant les tables SLR(1) pour G est appelé analyseur SLR(1) pour G et une grammaire ayant des tables d'analyse SLR(1) est SLR(1). Nous omettons en general le (1) apres SLR, car nous ne considérerons pas ici d'analyseurs utilisant plus d'un symbole de prévision. Exemple 4.26. Construisons les tables SLR pour la grammaire G' de l'exemple 4.22. La collection canonique des ensembles d'items LR(0) pour G' est représentée dans l'exemple 4.24. Considérons tout d'abord l'ensemble d'items I0: I0 :E' → •E E → •E+T E → •T T → •T*F Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 l'item F → • (E) produit l'entée Action[0,(] = décaler 4 et l'item F→•id produit l'action[0,id] = 75 Analyse syntaxique T → •F F → • (E) F → • id décaler5 les autres items de I0 ne produisent aucune action. Considérons I1 E' → E• E → E•+T Le premier Item produit Action [1,$] = accepter et le second item produit Action[1,+] = décaler 6. Considérons maintenant I2 E → T• T → T•*F comme Suivant(E) = {$,+,)}, le premier item produit Action[2,$] = Action[2,+] = Action[2,)] = réduire par E → T Le second item produit Action [2,*] = décaler 7 (car transition (I1,*) = I7 dans le DFA) En continuant ainsi, nous obtenons les tables Action et successeur suivantes: Action id 0 1 2 3 4 5 6 7 8 9 10 11 + * d5 ( ) $ r2 r4 Acc r2 r4 r6 r6 d4 d6 r2 r4 d7 r4 r6 r6 d5 d4 d5 d5 d4 d4 d6 r1 r3 r5 d7 r3 r5 d11 r1 r3 r5 Successeur E T 1 2 F 3 8 2 3 9 3 10 r1 r3 r5 Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 76 Traduction dirigée par la syntaxe Traduction dirigée par la syntaxe Il s'agit d'associer de l'information à une construction d'un langage de programmation en attachant des attributs aux symboles de la grammaire représentant cette construction. Les valeurs de ces attributs sont calculées par des règles sémantiques" associés aux productions de la grammaire. Il existe deux notations pour associer des règles sémantiques aux productions : les définitions dirigées par la syntaxe et les schémas de traduction. Les premiers ne spécifient pas l'ordre dans lequel la traduction s'effectue, les schémas de traduction indiquent l'ordre dans lequel les règles sémantiques doivent être évaluées. Ces deux notations sont employées pour : - des vérifications sémantiques (détermination des types) - engendrer du code intermédiaire Dans les deux notations, on effectue l'analyse syntaxique du flot des terminaux d'entrée, on construit l'ordre syntaxique, puis ce dernier est parcouru autant de fois qu'il est nécessaire pour évaluer les règles sémantiques à ses nœuds l'évolution des règles sémantiques peut produire du code, sauvegarder de l'information dans une table de symboles, émettre des messages d'erreurs ou autres. Notation postfixée : La notation postfixée pour une expression E peut être définie récursivement comme suit : 1. Si E est une variable ou une constante, la notation postfixée de E est E ellemême. 2. Si E est une expression de la forme E1 op E2, où op est un opérateur binaire, la notation postfixée de E est E 1' E '2 op, où E 1' et E '2 sont les notations postfixées de E1 et E2 respectivement. 3. Si E est une expression de la forme (E1), la notation postfixée de E est la notation postfixée de E1. Remarque. Aucune parenthèse n'est nécessaire en notation postfixée, car la position et l'arité (nombre d'arguments) des opérateurs ni permettent qu'un seul décodage de l'expression postfixée. Par exemple, la notation postfixée (9 - 5) + 2 est 95 -2+ et celle de 9 – (5+2) est 952+-. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 77 Traduction dirigée par la syntaxe 5.1. Définitions dirigées par la syntaxe Une définition dirigée par la syntaxe utilise une grammaire non contextuelle pour spécifier la structure syntaxique du texte d'entrée. A chaque symbole de la grammaire, on associe un ensemble d'attributs et, à chaque production, un ensemble de règles sémantiques pour calculer la valeur des attributs associés aux symboles apparaissant dans cette production. La grammaire et l'ensemble des règles sémantiques constituent la définition dirigée par la syntaxe. 5.1.1. Attributs synthétisés Les attributs synthétisés sont utilisés intensivement en pratique. Une définition dirigée par la syntaxe qui utilise uniquement des attributs synthétisés est appelée définition S-attribuée. Un attribut est synthétisé si sa valeur, à un nœud d'un arbre syntaxique, est déterminée à partir de valeurs d'attributs des fils de ce nœud. Les attributs synthétisés ont la propriété de pouvoir être évolués au cours d'un simple parcours ascendant de l'arbre syntaxique. Exemple 5.1. La figure 5.1 représente une définition dirigée par la syntaxe pour traduire des expressions formées de chiffres séparés par des signes + ou – en une notation postfixée. Est associé à chaque non terminal un attribut t dont la valeur est une chaîne qui représente la notation postfixée de l'expression engendrée par ce terminal dans un arbre syntaxique. Production expr → expr1 + terme expr → expr1 – terme expr → terme terme → 0 terme → 1 --- terme → 9 Règle sémantique expr.t : = expr1.t ⏐⏐terme.t⏐⏐'+' expr.t : = expr1.t ⏐⏐terme.t⏐⏐'-' expr. t : = terme.t eerme.t : = '0' terme.t : = '1' --terme.t : = '9' Figure 5.1. Définition dirigée par la syntaxe de la traduction infixée, postfixée (L'opérateur ⏐⏐ dans les règles sémantiques représente la concaténation de chaînes.) - La notation postfixée d'un chiffre est lui-même, la règle sémantique associée à la production « terme → 9 » définit terme.t comme étant 9, à chaque fois que cette production est utilisée à un nœud d'un arbre syntaxique. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 78 Traduction dirigée par la syntaxe - - Quand la production « exp → terme » s'applique, la valeur de terme.t devient la valeur de expr.t. La production « expr → expr1 + terme » dérive une expression formée d'un opérateur + ; l'indice de expr1 distingue l'instance de expr de droite de celle en partie gauche. La règle sémantique : « expr.t : = expr1.t ⏐⏐terme.t⏐⏐'+' » associée à la production définit la valeur de l'attribut expr.t en concaténant les formes postfixées expr1.t et le terme.t et en y ajoutant le signe plus. La figure 5.2 donne l'arbre décoré pour l'expression 9-5+2. La valeur de l'attribut t associé à chaque nœud a été calculée en utilisant la règle sémantique de la production utilisée au nœud. La valeur de l'attribut à la racine est la notation postfixée de la chaîne produite par l'arbre syntaxique. expr.t = 95 – 2 + expr.t = 95 – expr.t = 9 terme.t = 2 terme.t = 5 terme.t = 9 9 - 5 + 2 Figure 5.2 : Valeurs des attributs aux nœuds d'un arbre syntaxique 5.1.2. Parcours en profondeur Une définition dirigée par la syntaxe n'impose aucun ordre spécifique pour l'évaluation des attributs sur l'ordre syntaxique ; tout ordre d'évolution qui calcule l'attribut a après tous les attributs dont a dépend est acceptable. On peut utiliser le parcours en profondeur. Le parcours d'un arbre débute à la racine et récursivement, visite les fils de chaque nœud dans un ordre gauche-droite, comme le montre la figure 5.3. On évalue les règles sémantiques à un nœud donné après que tous les descendants de ce nœud ont été visités. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 79 Traduction dirigée par la syntaxe Procédure visiter (n : nœud) ; début pour chaque fils m de n, de gauche à droite faire visiter (m) ; évaluer les règles sémantiques au nœud n fin Figure 5.3. Parcours en profondeur d'un arbre 5.2. Schéma de traduction Un schéma de traduction est une grammaire non contextuelle dans laquelle on insère des fragments de programmes appelés actions sémantiques à l'intérieur même des parties droites des productions. Un schéma de traduction ressemble à une définition dirigée par la syntaxe, excepté que l'ordre d'évaluation des règles sémantiques est explicitement donné. On indique la position à laquelle une action doit être exécutée en englobant cette action entre accolades à laquelle une action doit être exécute en englobant cette action entre accolades et en l'écrivant à l'intérieur de la partie droite d'une production comme dans : reste → +terme {Imprimer ('+')} reste 1 Les actions sémantiques seront alors exécutés dans l'ordre où elles apparaissent au cours d'un parcours en profondeur de l'arbre syntaxique. Quand on construit un arbre syntaxique pour un schéma de traduction, on indique une action en construisant pour elle une feuille additionnelle, reliée par un trait pointille au nœud de la partie gauche de cette production. La figure 5.4 représente la portion de l'arbre syntaxique pour reste → +terme {Imprimer ('+')} reste 1 et son action Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 80 Traduction dirigée par la syntaxe reste + terme {Imprimer ('+')} reste 1 Figure 5.4. Portion d’un arbre syntaxique Le nœud pour une action sémantique n'a pas de fils, l'action est exécutée quand ce nœud est visité pour la première fois. Remarque. Les définitions dirigées par la syntaxe mentionnées jusqu'à maintenant ont la propriété suivante : la chaîne représentant la traduction du non-terminal en partie gauche de chaque production est la concaténation des traduction des non terminaux en partie droite dans le même ordre que dans la production, avec éventuellement des chaînes venues s'insérer entre ces traductions. Une définition dirigée par la syntaxe qui a cette propriété est simple. Exemple 5.2. Production Expr → expr 1 + terme Reste → + terme reste 1 Règle sémantique expr.t : = expr1.t ⏐⏐terme.t⏐⏐'+' reste.t : = terme.t ⏐⏐'+'⏐⏐ reste1.t chaîne additionnelle apparaît avant Les définitions dirigées par la syntaxe simple peuvent être implantées par des schémas de traduction dans lesquelles les actions impriment les draines additionnelles dans l'ordre dans lesquels ils apparaissent dans la définition. Exemples 5.3. La figure 5.5 représente un schéma de traduction dérivé de la définition de la figure 5.1. expr → expr + terme {Imprimer ('+')} expr → expr - terme {Imprimer '-'} expr → terme terme → 0 {Imprimer ('0')} terme → 1 {Imprimer ('1')} --terme → 9 {Imprimer ('9')} Figure 5.5. Actions traduisant des expressions en notation postfixée Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 81 Traduction dirigée par la syntaxe La figure 5.6 représente un arbre syntaxique avec des actions pour 9 – 5 + 2. expr expr expr - terme terme {Imprimer ('-')} terme 9 + 2 {Imprimer ('+')} {Imprimer ('2')} 5 {Imprimer ('5')} {Imprimer ('9')} Figure 5.6. Actions traduisant 9 – 5 + 2 en 95 – 2 + Quand elles sont exécutées au cours d'un parcours en profondeur de l'arbre syntaxique, les actions de la figure 5.6 impriment 95 – 2 +. 5.3. Arbre abstrait et arbre concret Dans un arbre syntaxique abstrait, chaque nœud représente un opérateur et les fils de ce nœud représentent des opérandes. Par opposition, un arbre syntaxique est appelé arbre syntaxique concret et la grammaire sous jacents est appelée syntaxe concrète du langage. Par exemple les figures 5.7 et 5.8 donnent les arbres abstraits et syntaxiques pour 9 – 5 + 2. + _ liste 2 liste chiffre 9 5 liste chiffre chiffre 9 Figure 5. 7. Arbre abstrait pour 9 – 5 + 2 _ 5 + 2 Figure 5.8. Arbre syntaxique pour 9 – 5 + 2 Remarque. Il est souhaitable qu'un schéma de traduction s'appuie sur une grammaire dont les arbres syntaxiques sont aussi près que possible des arbres abstraits. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 82 Traduction dirigée par la syntaxe Remarque. On peut désigner une définition dirigée par la syntaxe par SDTS abstraite pour syntax Directed Translation schéma abstrait et le schéma de traduction tel qu'il a été présenté SDTS concret. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 83 Contrôle de type Contrôle de type De nombreux compilateurs Pascal combinent les contrôles statiques et la production de code intermédiaire avec l'analyse syntaxique. Pour des constructions plus complexes comme celles d'Ada, on peut faire intervenir une passe séparée pour le contrôle de type, placée entre l'analyse syntaxique et la production de code Intermédiaire, comme indiqué dans la figure 6.1. Figure 6.1. Position du contrôleur de type Un contrôleur de type vérifie que le type d'une construction correspond au type attendu par son contexte. Ainsi, par exemple, l'opérateur arithmétique « mod » de Pascal nécessite des opérandes entiers j un contrôleur de type doit donc vérifier que les opérandes de « mod » sont bien de type entier. De façon similaire, le contrôleur de type doit vérifier que le déréférençage n'est appliqué qu'à des pointeurs, que seuls des tableaux sont indicés, que toute fonction définie par l'utilisateur est utilisée avec le bon nombre d'arguments de types corrects, ainsi de suite. 6.1. Expression de type Le type de toute construction d'un langage est dénoté par une « expression de type ». De façon informelle, ou bien une expression de type est un type de base, ou bien elle est formée en appliquant un opérateur appelé « constructeur de type» à d'autres types de base. L'ensemble des types de base et des constructeurs dépend du langage sur lequel s'effectue le contrôle. Nous utilisons dans ce chapitre la définition suivante d'expression de type: 1. Un type de base est une expression de type. Parmi les types de base, on trouve booléen, caractère, entier et réel. Un type de base spécial, erreur_de_type, signale les erreurs détectées lors du contrôle de type. Enfin, un type de base vide, dénotant « l'absence de valeur », permet le contrôle des instructions. 2. Puisque les expressions de type peuvent être nommées, un nom de type est une expression de type. 3. Un constructeur de type appliqué à des expressions de type est une expression de type. On trouve parmi les constructeurs: (a) Les tableaux. Si T est une expression de type, tableau (I, T) est une expression de type, qui dénote le type d'un tableau dont les éléments sont du type T et dont l'ensemble des indices est I. Ce dernier est souvent un intervalle d'entiers. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 84 Contrôle de type (b) Les produits. Si Tl et T2 sont des expressions de type, leur produit cartésien Tl X T2 est une expression de type. Nous supposons que X est associatif à gauche. (c) Les structures. En un certain sens, le type d'une structure est le produit des types de ses champs. La différence entre une structure et un produit est que les champs d'une structure ont des noms. (d) Les pointeurs. Si T est une expression de type, pointeur(T) est une expression de type dénotant le type «pointeur vers un objet de type T ». (e) Les fonctions. En termes mathématiques, une fonction fait correspondre à des éléments d'un ensemble de départ, le domaine, des éléments d'un ensemble d'arrivée, le co-domaine. Nous pouvons considérer que les fonctions des langages de programmation font correspondre au type domaine D le type codomaine A. Le type d'une telle fonction sera dénoté par l'expression de type DÆA. Par exemple, la fonction intrinsèque « mod » de Pascal a pour type domaine entier X entier, c'est-à-dire un couple d'entiers, et pour type codomaine entier. Nous disons donc que « mod » a pour type : entier X entier Æ entier. 6.2. Spécification d’un contrôleur de type simple Nous spécifions dans cette section un contrôleur de type pour un langage simple, dans lequel le type de tout identificateur doit être déclaré avant que cet identificateur soit utilisé. Ce contrôleur de type est un schéma de traduction qui synthétise le type de toute expression à partir des types de ses sous-expressions. Le contrôleur de type peut manipuler tableaux, pointeurs, instructions et fonctions. 6.2.1. Un langage simple Soit la grammaire G suivante : PÆ D ; E D Æ D ; D | id : T T Æ caractère |entier |tableau [nb] de T | ↑T E Æ littéral |nb |id|E mod E|E [ E ]|E↑ Cette grammaire engendre des programmes, représentés par le non-terminal P, qui consistent en une séquence de déclarations D suivie d'une seule expression E. Voici un exemple de programme engendré par la grammaire G : x : entier ; x mod 1999 Avant de nous intéresser aux expressions, considérons les types dans ce langage. Le langage lui-même a deux types de base, caractère et entier; nous utilisons un troisième type de base, erreur_de_type, pour signaler les erreurs. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 85 Contrôle de type Dans le schéma de traduction de la figure 6.2, l'action associée à la production DÆid:T stocke un type dans l'entrée d'un identificateur dans une table de symboles. L'action AjouterType(id.entrée, T.type) est appliquée à l'attribut synthétisé entrée, qui pointe vers l'entrée de id dans la table des symboles, et à une expression de type représentée par l'attribut synthétisé type du non-terminal T. Figure 6.2. La partie d’un schéma de traduction qui stocke le type d’un identificateur Si T engendre caractère ou entier, T.type est défini comme caractère ou entier, respectivement. La borne supérieure d'un tableau est obtenue à partir de l'attribut val de l'unité lexicale nb qui donne l'entier représenté par nb. Les tableaux sont censés commencer à 1, donc le constructeur de type tableau est appliqué à l'intervalle l..nb.val et au type des éléments. Puisque D apparaît avant E en partie droite de P Æ D ; E, nous sommes sûrs que le type de tous les identificateurs déclarés sera stocké avant que commence le contrôle de l'expression engendrée par E. En fait, en modifiant la grammaire G de façon adéquate, nous pouvons mettre en œuvre les schémas de traduction de cette section lors d'une analyse syntaxique ascendante ou descendante, au choix. 6.2.2. Contrôle de type des expressions Dans les règles qui suivent, l'attribut synthétisé type associé à E donne l'expression de type affectée par le système de typage à l'expression engendrée par E. Les règles sémantiques ci-dessous spécifient que les constantes représentées par les unités lexicales littéral et nb sont de types caractère et entier respectivement: Nous utilisons une fonction Rechercher(e) pour retrouver le type stocké dans l'entrée référencée par e dans la table des symboles. Lorsqu'un identificateur apparaît dans une expression, nous allons chercher son type déclaré et l'affectons à l'attribut type: Une expression formée par application de l'opérateur « mod » à deux sousexpressions de type entier a pour type entier; dans les autres cas son type est erreur_de_type. La règle est la suivante: Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 86 Contrôle de type Dans un accès à un élément de tableau E1[E2], l'expression E2 doit être de type entier, auquel cas le résultat est le type t des éléments obtenu à partir du type tableau(s,t) de E1; nous n'utilisons pas l'ensemble s des indices du tableau. Dans les expressions, l'opérateur postfixe ↑ retourne l'objet référencé par son opérande. Le type de E↑ est le type t de l'objet référencé par le pointeur E: 6.2.3. Contrôle de type des instructions Puisque les constructions des langages telles les instructions n'ont en général pas de valeur associée, le type de base spécial vide peut leur être affecté. Lorsqu'une erreur est détectée dans une instruction, le type qui est affecté à cette instruction est erreur_de_type. Les instructions que nous prenons en compte sont l'affectation, la conditionnelle et la boucle «tant que». Les séquences d'instructions sont séparées par des points-virgules. Les productions de la figure 6.3 peuvent être combinées avec celles de la grammaire G, en changeant simplement la production pour un programme complet en PÆD;I. Un programme consiste alors en des déclarations suivies d'instructions ; nous avons toujours besoin des règles précédentes pour le contrôle des expressions, car les instructions peuvent contenir des expressions. Les règles pour le contrôle des instructions sont données à la figure 6.3. La première règle contrôle que les parties gauche et droite d'une instruction d'affectation sont du même type. Les deuxième et troisième règles spécifient que, dans l'instruction conditionnelle et la boucle « tant que », l'expression doit être de type booléen. Les erreurs sont propagées par la dernière règle de la figure 6.3, parce qu'une séquence d'instructions n'est de type vide que si chaque sous-instruction est de type vide. Dans ces règles, une discordance de types produit le type erreur_de_type ; bien entendu, un contrôleur de type convivial signalerait en outre la nature et l'emplacement de la discordance de types. Figure 6.3. Schéma de traduction pour le contrôle de type des instructions Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 87 Contrôle de type 6.3. Conversion de type Considérons une expression comme x+i où x est de type réel et i de type entier. Puisque les représentations des entiers et des réels ne sont pas les mêmes dans un calculateur et que l'on utilise des instructions machine différentes pour les opérations sur les entiers et sur les réels, le compilateur peut avoir à convertir un des opérandes de + pour assurer que les deux opérandes sont du même type lorsque l'addition est effectuée. La définition du langage spécifie quelles sont les conversions nécessaires. Lorsqu'un entier est affecté à un réel, ou vice versa, la conversion se fait vers le type de la partie gauche de l'affectation. Dans les expressions, la transformation consiste habituellement à convertir l'entier en un nombre réel, puis à effectuer une opération réelle sur le couple d'opérandes réels ainsi obtenu. Dans un compilateur, le contrôleur de type peut être utilisé pour insérer ces opérations de conversion dans la représentation intermédiaire du programme source. Par exemple, la notation postfixée de x+i pourrait être: x i EntierVersRéel +réel Ici, l'opérateur EntierVersRéel convertit i d'entier vers réel, puis +réel effectue une addition réelle sur ses opérandes. Coercitions La conversion d'un type vers un autre est dite implicite si elle est censée être réalisée automatiquement par le compilateur. Les conversions de type implicites, que l'on appelle aussi coercitions, sont dans de nombreux langages limitées aux situations où elles ne donnent en principe lieu à aucune perte d'information j par exemple, un entier peut être converti en un réel mais pas le contraire. En pratique toutefois, une perte d'information est possible si un nombre réel occupe le même nombre de bits qu'un nombre entier. La conversion est dite explicite si le programmeur a quelque chose à écrire pour que la conversion ait lieu. En Ada, toutes les conversions nécessaires en pratique sont explicites. Du point de vue du contrôleur de type, les conversions explicites sont exactement semblables aux applications de fonctions et ne présentent donc aucun problème nouveau. En Pascal, par exemple, la fonction intrinsèque ord fait correspondre à un caractère un entier et chr effectue la correspondance inverse d'entier vers caractère j ces conversions sont donc explicites. En revanche, C contraint (c'est-à-dire convertit implicitement) les caractères ASCII en des entiers compris entre 0 et 127 dans les expressions arithmétiques. Exemple 6.1. Considérions les expressions formées en appliquant un opérateur arithmétique op à des constantes et des identificateurs, comme dans la grammaire de la figure 6.4. Supposons que nous ayons deux types réel et entier, les entiers étant convertis en des réels lorsque cela est nécessaire. L'attribut type du non-terminal E peut avoir pour valeur soit entier soit réel; les règles de contrôle de type sont données à la figure 6.4. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 88 Contrôle de type La fonction Rechercher(e) retourne le type stocké dans l'entrée référencée par e dans la table des symboles. Figure 6.4. Règles de contrôle de type pour la coercition d’entier vers réel Affectation de type mixte x : real y : real I : integer J : integer X:=Y+I*J code 3 Q voulu T1 : = I * J T2 : = intoreal T1 T3 : = y + T2 X : = T3 E → E1 op E2 E.place : = New Temp ( ) ; if E1.type = E2.type = integer then begin E.type : = integer GEN (E.place : = E1.place "intop" E2.place) end else if (E1.type = real) and (E2.type = real) then begin E.type : = real GEN (E.place : = E1.place "real op" E2place) end else if F2.type = real then begin U : = NewTemp ( ) ; GEN (U : = "intoreal" E1.place) GEN (E.place : = U "realop" E2.place E.type : = real end else // E2.type = integer begin U : = NewTemp ( ) ; GEN (U : = intoreal E2.place) Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 89 Contrôle de type GEN (E.place : = E1.place "realop" U) E.type : = real end end if end if end if E → (E1) E → − E1 • E.type : = E1.type E.place : = E1.place E.type : = E1.type E.place : = NewTemp ( ) • E → id GEN (E.place : = " − " E1.place) E.type : = Rechercher (id.entrée) E.place : = id.place A → id : = E if rechercher (id.entrée) = real then if E.type = real then GEN (id.place : = E.place) else // E.type = integer begin U : = NewTemp ( ) GEN (U : = intoreal (E.place)) GEN (id.place : = U) end else // id son type est entier if E.type ) integer then GEN (id.place : = E.place) else erreur ( ) end if end if Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 90 Production de code intermédiaire Production de code intermédiaire A l'issue de l'analyse syntaxique et de l'analyse sémantique, certains compilateurs construisent explicitement une représentation intermédiaire du programme source. On peut considérer celle ci comme un programme pour une machine abstraite (pas de limites quand au nombre de registres). Cette représentation intermédiaire doit avoir deux propriétés importantes: Elle doit être facile à produire et facile à traduire en langage cible. L'une des formes de ce code intermédiaire est le code à 3 adresses 7.1. Code à trois adresses Un code à trois adresses est une séquence d'instructions de la forme: x := y op z où x,y et z sont des noms, des constantes ou des variables temporaires produites par le compilateur. « op » dénote n'importe quel opérateur, tel qu'un opérateur arithmétique, entier ou réel, ou bien un opérateur logique sur des valeurs booléennes. Ainsi l'expression du langage source x +y*z pourra être traduite par séquence suivante: t1 := y *z t2 := x + t1 où t1 et t2 sont des variables temporaires produites par le compilateur. Remarque. Le code intermédiaire est une représentation linéarisée d'un arbre abstrait, dans laquelle les noms explicites correspondent aux nœuds internes de l'arbre. La terminologie "code à trois adresses" peut se justifier par le fait que chaque instruction contient en général trois adresses, deux pour les opérandes et une pour le résultat. Dans les implantations de code à trois adresses plus loin dans ce chapitre, un nom définit par le programmeur est représenté par un pointeur référençant une entrée dans la table des symboles. 7.1.1. Traduction dirigée par la syntaxe pour la production du code à trois adresses Quand on produit du code à trois adresses, on crée des variables temporaires pour les nœuds internes d'un arbre abstrait. La valeur du non terminal E partie gauche de la production E → E1 + E2 est calculée dans un nouveau temporaire t. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 91 Production de code intermédiaire En général, le code à trois adresses pour id := E est constitué par la séquence de l'évaluation de E dans un temporaire t, suivis par l'affectation id.place := t. Si une expression est un identificateur simple, par exemple y, alors y lui même contient la valeur de l'expression. La définition S-attribuée (qui utilise uniquement des attributs synthétisés) de la figure 7.1 produit du code à trois adresses pour les instructions d'affectation. Etant donné l'instruction d'entrée a := b*-c+b*-c, elle produit la séquence: t1 := -C t2 := b*t1 t3 := -C t4 := b + t3 t5 := t2 + t4 a := 5 Production I → id := E E → E1 + E2 E → E1 * E2 E → -E1 E → (E1) E → id Règle Sémantique I.code := E.code ||Gen(id.place ':= ' E.place) E.place := NewTemp; E.code := E1.code||E2.code|| Gen (E.place ':=' E1.place '+' E2.place) E;place := NewTemp; E.code := E1.code||E2.code||Gen(E.place ':=' E1.place '*' E2.place) E.place := Newtemp; E.code := E1.code||Gen(E.place ':=' 'MoinsU' E1.place) E.place := E1.place E.code := E1.code; E.place := id.place; E.code := ' ' Figure 7.1. Une définition S-attribuée produisant du code à trois adresses pour les instructions d'affectation. L'attribut synthétisé I.code représente le code 3@ pour l'instruction d'affectation définie par le non terminal I. Le non terminal E possède deux attributs: - E.place : le nom qui contient la valeur de E et - E.code : la séquence de code à trois adresses permettant d'évaluer E. La fonction NewTemp retourne un nouveau nom à chaque appel. Chaque nom est de la forme tn. Nous utilisons Gen(x ':=' y '+' z) pour représenter l'instruction à trois adresses x':='y'+'z. Les expressions apparaissant à la place de variables comme x,y et z sont évaluées quand-elles sont passées en paramètre à Gen. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 92 Production de code intermédiaire 7.1.2. Implantation d'instructions à trois adresses Dans un compilateur, les instructions à 3 adresses peuvent être implémentées par des structures dont les champs contiennent l'opérateur et les opérandes. Parmi ces représentations on trouve les quadruplets et les triplets. Quadruplets. Un quadruplet est une structure possédant quatre champs appelés respectivement op, arg1, arg2 et résultat. Le champ op contient un code interne pour l'opérateur. L'instruction à trois adresses x := y op z est représentée en plaçant y dans arg1, z dans arg2 et x dans résultat. Les instructions utilisant les opérateurs unaires tels que x := -y ou x := y n'utilisent pas arg2. Les instructions de branchement conditionnel (if A relop B Goto L) et inconditionnel (Goto L) rangent leurs étiquettes dans résultat. Exemple 7.1. Les quadruplets de la figure 7.2 (a) représentant l'instruction d'affectation a:= b*c+b*-c. Elles correspondent au code à trois adresses. t1 := -c t2 := b *t1 t3 := -c t4:= b*t3 t5 := t2 +t4 a := t5 op (0) MoinsU (1) * (2) MoinsU (3) * (4) + (5) := (a) Quadruplets arg1 c b c b t2 t5 arg2 t1 t3 t4 résultat t1 t2 t3 t4 t5 a (0) (1) (2) (3) (4) (5) op arg1 MoinsU c * b MoinsU c * b + (1) := a (b) triplets arg2 (0) (2) (3) (4) Figure 7.2. Représentation de code à trios adresses en quadruplets et triplets Les contenus des champs arg1, arg2 et résultat sont des pointeurs vers les entrées de la table des symboles des noms représentées par ces champs. Les variables temporaires doivent donc être rangées dans la table des symboles lorsqu'elles sont crée. Triplets. On peut éviter de ranger des variables temporaires dans la table des symboles en référençant une valeur temporaire par la position de l'instruction qui la calcule. Dans ce cas, les instructions à 3 adresses sont représentées par les structures à 3 champs: op arg1, arg2 comme dans la figure 7.2 (b). Les arguments arg1 et arg2 Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 93 Production de code intermédiaire sont soit des pointeurs vers la table des symboles, soit des pointeurs vers la structure des triplets (pour les valeurs temporaires). Voici maintenant deux techniques équivalentes pour traduire l'instruction d'affectation et les expressions arithmétiques en code à 3 adresses. Définition dirigée par la syntaxe pour l'affectation et les expressions arithmétiques. (SDTS abstraite) AF → id := E E → E1 op E2 E → -E1 E → (E1) E → id A.code := E.code || id.place ':=' E.place E.place := newTemp E.code := E1.code|E2.code|E.place ':=' E1.place 'op' E2.place E.place := newtemp(); E.code := E1.code||E.place ':=' '-' E1.place E.place := E1.place E.code := E1.code E.place := id.place E.code := Null Figure 7.3. SDTS abstraite pour traduire l'instruction d'affectation et les expressions arithmétiques Schéma de traduction pour l'affectation et les expressions arithmétiques (SDTS concrète) AF → id := E E → E1 op E2 {Gen(id.place ':= ' E.place)} E → (E1) {E.place := newTemp(); Gen(E.place ':=' E1.place 'op' E2.place)} {E.place := NewTemp(); Gen(E.place ':=' '-' E1.place)} {E.place ':=' E1.place} E → id {E.place ':=' id.place} E → -E1 Figure 7.4. SDTS concrète pour traduire l'instruction d'affectation et les expressions arithmétiques 7.2. Expressions booléennes (méthode numérique) Une méthode de représentation des expressions booléennes consiste à coder numériquement vrai et faux et à évaluer une expression booléenne comme une Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 94 Production de code intermédiaire expression arithmétique. On utilise souvent la valeur 1 pour représenter vrai et 0 pour représenter faux. Voici un exemple de traduction: a ou b et non C ("ou "et "et" associatifs à gauche, "ou" a une priorité inférieur à "et" et à "non") Devient : t1 := non C t2 := b et t1 t3 := a ou t2 Une expression de relation telle que a < b est équivalente à l'expression conditionnelle si a <b alors 1 sinon 0 qui peut être traduite par la séquence à trois adresses suivante: 100: si a < b aller à 103 101: t:= 0 102: aller à 104 103: t:= 1 104: Le schéma de traduction permettant de produire du code à trois adresses pour les expressions booléennes est présenté à la figure 7.5. Dans ce schéma, on suppose que la procédure "Gen" écrit les instructions à trois adresses sur un fichier de sortie, InstSuiv fournit l'indice de la prochaine instruction à trois adresses dans la séquence de sortie et que "Gen" incrémente InstSuiv après avoir produit les instructions à 3 adresses. E → E1 ou E2 E → E1 et E2 E → non E1 E→ (E1) E → id1 oprel id2 E → vrai E→Faux {E.place := NewTemp; Gen( E.place ':=' E1.place 'ou' E2.place)} E.place := NewTemp; Gen(E.place ':=' E1.place 'et' E2.place)} {E.place := NewTemp; Gen(E.place ':=' 'non' E1.place)} {E.place := E1.place)} {E.place := NewTemp; Gen('Si' id1.place oprel.op id2.place 'aller à' InstSuiv +3); Gen(E.place ':=' '0'); Gen('aller à' InstSuiv +2); Gen(E.place ':=' '1')} {E.place := NewTemp; Gen(E.place ':=' '0')} {E.place := NewTemp Figure 7.5. Schéma de Traduction utilisant la représentation numérique des booléens Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 95 Production de code intermédiaire 7.3. Techniques de reprise Arrière (BackPatching) Dans cette section, nous montrons comment la technique de reprise arrière peut être utilisée pour engendrer le code des expressions booléennes et des instructions de flot de contrôle en une seule passe. Principe de base. Le principe consiste à produire une série de branchements dont les destinations sont temporairement indéfinies. Ces instructions sont conservées dans une liste d'instructions de branchements dont les champs "étiquettes" sont remplis quand les valeurs des étiquettes seront déterminées Exemple 7.2. vrai si A < B alors A := B+C Faux Nous produirons les quadruplets dans un tableau. Les étiquettes seront les indices de ce tableau. 1 2 3 4 5 si A <B aller à Vrai Aller à faux T1 := B+C A := T1 La production des quadruplets pour A := B+C nous a permis de connaître le numéro du quadruplet avec lequel commence cette instruction qui est 3. ⇒ Le branchement indéfini de la ligne 1 devrait alors brancher vers le quadruplet 3 Aussi on a pu savoir que l'adresse du premier quadruplet qui suit le code pour A := B+C est 5. Le branchement indéfini de la ligne 2 devrait brancher vers 5. Le code devient alors: 1 si A <B aller à 3 2 3 4 5 Aller à 5 T1:= B+C A := T1 Æ C'est le principe de reprise arrière. Nous allons utiliser trois fonctions pour manipuler les listes d'instructions: 1. MakeList(i) crée une nouvelle liste contenant seulement i, indice dans le tableau des quadruplets; makelist retourne un pointeur vers la liste ainsi crée. 2. Merge (p1,p2) concatène les listes pointées par p1 et p2 et retourne un pointeur vers la liste résultat. 3. BackPatch (p,i) insère i comme étiquette cible dans chacune des instructions de la liste pointée par p. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 96 Production de code intermédiaire 7.3.1. Traduction des expressions booléennes Nous présentons un schéma de traduction destiné à la production de code pour les expressions booléennes pendant une analyse ascendante. Nous insérons le non terminal marqueur M dans la grammaire afin que l'action sémantique associée puisse collecter la valeur de l'indice du prochain quadruplet à produire. La grammaire que nous utilisons est la suivante: C→ C1 ou C2 |C1 et MC2 |non C1 |(C1) |id1 oprel id2 |bid M →ε On utilise les attributs synthétisés True et False attachés au non terminal C pour produire le code des expressions booléennes. Quand on produit le code pour C, on ne produit pas complètement les instructions de branchement correspondant aux sorties vrai et faux car leurs champs étiquette n'est pas rempli. On range ces instructions de branchement incomplètes dans les listes pointées par C.true et C.false. Les actions sémantiques sont expliquées comme suit: True C → C1 True False et C2 False Etudions la production C C1 et MC. Si C1 est faux, C est faux aussi; Les instructions de C1.False appartiennent donc aux instructions de C.False. Si C1 est vrai, il faut tester C2, donc l'instruction destination de C1.true doit débuter le code produit par C2. On obtient cette instruction destination en utilisant le Marqueur M. L'attribut M.quad mémorise le numéro de la première instruction de C2.code. A la production M→ε on associe l'action sémantique {M.quad = nextQuad. La variable globale nextQuad contient la valeur de l'indice du prochain quadruplet qui sera produit. Cette valeur sera affectée à C1.True quand le reste de la production C C1→et M C2 aura été traité. Le schéma de Traduction complet est le suivant: (1) C→ C1 ou C2 (2) C→ C1 et MC2 { BackPartch(C1.False, M.quad); C.True := Merge(C1.True, C2.True); C.False} { BackPatch(C1.True, M.quad); Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 97 Production de code intermédiaire C.True := C2.true C.False := Merge(C1.false, C2.False)} (3) C→ non C1 { C.True := C1.false; C.false := C1.True} (4) C→ (C1) { C.True := C1.True C.False := C1.False} (5) C → id1 oprel id2 (6) C → bid { C.True := MakeList(nextQuad); C.False := MakeList (nextQuad+1); GEN('Si' id1.place oprel.op id2.place 'aller à GEN('aller à ')} ') { C.true := makeList (nextQuad); C.false := makeList (nextQuad +1); GEN('Si' bid.place 'aller à ');} Remarque. L’action sémantique (5) produit deux instructions, un branchement conditionnel et un branchement inconditionnel. Aucun des champs étiquette n'est rempli dans ces deux instructions. On range l'indice de la première instruction produite dans une liste et on donne à C.True la valeur du pointeur vers cette liste. On range la seconde instruction produite ' aller à ' dans une liste et on donne à C.Faux la valeur du pointeur vers cette liste. 7.3.2. Traduction des instructions de flot de contrôle Nous allons développer un schéma de traduction pour les instructions engendrées par la grammaire suivante: P→I I → si C alors I |si C alors I sinon I |tantque C faire I |début L fin L → L;I |I P: programme C: Condition I: Instruction L:Liste d'instructions A: Affectation E: expression C→ C1 ou C2 |C1 et MC2 |non C1 |(C1) |id1 oprel id2 |bid A → bid := C |id := E Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 98 Production de code intermédiaire E →E+E|E*E| -E| (E) |id Approche générale consiste à remplir les champs des instructions de branchement quand les instructions destination sont connues. Outre les expressions booléennes, les instructions ont besoin de listes de branchements données par l'attribut I.next vers le code qui suit dans la séquence. True I → si C alors M1 I1 N sinon M2 I2 instruction qui suit I en ordre d'exécution False I.next = une liste contenant des numéros de quadruplets déjà générés qui sont des branchements conditionnels et non conditionnels dont l'étiquette est manquante et qui devraient aller à l'instruction qui suit I en ordre d'exécution. si le contrôle passe à la fin de I1, on doit inclure à la fin de I1 une instruction de branchement vers la fin de I. Nous utilisons le marqueur N pour produire ce saut. N → ε { N.nest := makelist(nextSuad) GEN ('aller à ') } N a un attribut N.next qui pointe vers la liste contenant l'unique quadruplet 'aller à ' qui a un branchement non défini Voici maintenant les règles sémantiques: I → si C alors M1I1N sinon M2I2 { Backpatch (C.True, M1.quad); BackPatch (C.False, M2.quad); M→ ε {M.quad := nextquad)} I.next := Merge (I1.next, N.next, I2.next)} Nous reportons les destinations des branchements quand C est vrai dans le quadruplet M1.quad(début I1) de la même façon, nous reportons les destinations des branchements quand C est faux dans le debut de I2. La liste I.next englobe tous les sauts dont la destination est située à l'extérieur de I1 et I2 ainsi que celui produit par N. de façon analogue True next I → si C alors M I1 false Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 99 Production de code intermédiaire I → si C alors M I1 {Backpatch(C.true, M.quad); I.next := Merge (C.False, I1.next)} Les Boucles True I → Tant que M1 C faire M2 aller à explicite I1 • false Figure 7.6 (a) I.début vers C.vrai C.code vers C.Faux C.vrai I1.code C.Faux aller à I.début Figure 7.6 (b) Dans l'implémentation de la figure 6 (b), les étiquettes I.début et C.vrai marquent le début du code associé à I et le début du code associé à I1. Les deux occurrences du marqueur M enregistrent les numéros de ces quadruplets. Lorsque le corps de la boucle I1 a été exécuté, le contrôle retourne au début de la boucle. Lorsque nous réduisons par la production I → Tantque M1C faire M2 I1 nous devons effectuer la reprise arrière (Backpatch) des éléments de la liste I1.next afin de remplir le champs destination par M1.quad. Un branchement explicite vers le début du code de C est ajouté à la suite du code de I1. C.true est repris en arrière afin d'atteindre le début de I1. Les branchements depuis C.True permettent alors d'atteindre M2.quad. I → Tant que M1 C faire M2 I1 { Backpatch (I1.next, M1.quad); Backpatch(c.true, M2.qua); I.next := C.false GEN('aller à ' M1.quad)} Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 100 Production de code intermédiaire de même pour la boucle répéter next True I → Repeter M1I1 Jusqu'à M2C false Action sémantique pour le marqueur M M→ ε {M.quad := nextquad)} Action sémantique pour répéter { Backpatch (I1.next, M2.quad) Backpatch (C.false, M1.quad) I.next := C.true } I → début L fin I→ A {I.next := L.next} {I.next := Makelist()} L'affectation L → L1;MI I.next := nil initialize I.next à la liste vide. {Backpatch (L1.next, M.quad); L.next := I.next} L'instruction qui suit L1 dans l'ordre d'exécution est le début de l'instruction I. Ainsi la liste L1.next est reprise en arrière au début du code de I, indiqué par M.quad. L → I {L.next := I.next}. 7.3.3. Traduction des déclarations Dans cette sections deux grammaires différentes concernant les déclaration 1 ère Grammaire de déclaration : D → integer NameList D → real NameList {Backpatch (Nalemist.next, int)} {Backpatch (Namelist.next, real) NameList → id, NameList1 { NameList.next := ADD(NameList1.next, id.place)} | id {NameList.next := makeList (id.place)} NameList est un non terminal qui désigne une liste d'identificateurs. L'attribut NameList.next désigne les identificateurs déjà rencontrés et dont le type est encore inconnu et qu'il faut backpatcher pour qu'on le trouve. ADD(p,i) est une fonction qui ajoute i à la liste pointée par p et retourne un pointeur sur la liste p mise à jour. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 101 Production de code intermédiaire 2ème Grammaire de déclaration : D→ integer id D → real id { Entrer (id.place, integer); D.attr := int} {Entrer (id.place, real); D.attr := real } D → D1, id {D.attr := D1.attr; Entrer (id.place, D1.attr)} (Entrer (id.place, T): recherche l'emplacement de id.place dans la table des symboles et insère le T dans le champ type) Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 102 Bibliographie Bibliographie D. Thalman, « Conception et implantation de langage de programmation: une introduction à la compilation », isbn : 0-88612-020-9, 1979. J. Voiron, « Comprendre la compilation ». A. Aho, « Compilers principles techniques and tools », isbn : 0-201-10194-7, AddisonWesley, 1986. A. Aho, « Compilateurs:Principes, Techniques et outils », isbn : 2-10-005126-1, Dunod, 2000. Y. Noelle, « Traitements des langages évolués: compilateur interprétation support d'exécution », isbn : 2-225-81368-X, Masson, 1988. H. Glaire, « Technique de compilation », isbn : 2-85428-119-5, CEPAD, 1989. R. Wilher, « Compilateurs (Les): théorie, construction génération », isbn : 2-22584615-4, Masson, 1984. Sami KHALFOUI – Moez HAMMAMI – ENSI 2006/2007 103