Vous êtes sur la page 1sur 67

Licence Universitaire Professionnelle

G enie Logiciel & Th eorie des Langages

Techniques et outils pour la compilation


Henri Garreta
Facult e des Sciences de Luminy - Universit e de la M editerran ee Janvier 2001

Table des mati` eres


1 Introduction 1.1 Structure de principe dun compilateur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Analyse lexicale 2.1 Expressions r eguli` eres . . . . . . . . . . . . . . . . . . . . . 2.1.1 D enitions . . . . . . . . . . . . . . . . . . . . . . . 2.1.2 Ce que les expressions r eguli` eres ne savent pas faire 2.2 Reconnaissance des unit es lexicales . . . . . . . . . . . . . . 2.2.1 Diagrammes de transition . . . . . . . . . . . . . . . 2.2.2 Analyseurs lexicaux programm es en dur  . . . . . 2.2.3 Automates nis . . . . . . . . . . . . . . . . . . . . . 2.3 Lex, un g en erateur danalyseurs lexicaux . . . . . . . . . . . 2.3.1 Structure dun chier source pour lex . . . . . . . . 2.3.2 Un exemple complet . . . . . . . . . . . . . . . . . . 2.3.3 Autres utilisations de lex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 5 5 5 7 8 8 9 12 14 14 16 18 19 19 19 20 21 24 24 25 26 26 29 29 30 31 31 33 35

3 Analyse syntaxique 3.1 Grammaires non contextuelles . . . . . . . . . . . . . . . . . . 3.1.1 D enitions . . . . . . . . . . . . . . . . . . . . . . . . 3.1.2 D erivations et arbres de d erivation . . . . . . . . . . . 3.1.3 Qualit es des grammaires en vue des analyseurs . . . . 3.1.4 Ce que les grammaires non contextuelles ne savent pas 3.2 Analyseurs descendants . . . . . . . . . . . . . . . . . . . . . 3.2.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.2 Analyseur descendant non r ecursif . . . . . . . . . . . 3.2.3 Analyse par descente r ecursive . . . . . . . . . . . . . 3.3 Analyseurs ascendants . . . . . . . . . . . . . . . . . . . . . . 3.3.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.2 Analyse LR(k ) . . . . . . . . . . . . . . . . . . . . . . 3.4 Yacc, un g en erateur danalyseurs syntaxiques . . . . . . . . . 3.4.1 Structure dun chier source pour yacc . . . . . . . . . 3.4.2 Actions s emantiques et valeurs des attributs . . . . . . 3.4.3 Conits et ambigu t es . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . faire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

4 Analyse s emantique 4.1 Repr esentation et reconnaissance des types . . 4.2 Dictionnaires (tables de symboles) . . . . . . . 4.2.1 Dictionnaire global & dictionnaire local 4.2.2 Tableau ` a acc` es s equentiel . . . . . . . . 4.2.3 Tableau tri e et recherche dichotomique . 4.2.4 Arbre binaire de recherche . . . . . . . . 4.2.5 Adressage dispers e . . . . . . . . . . . .

. . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

37 38 42 42 43 45 46 49 51 51 51 53 54 56 56 57 58 58 60 60 63 65

5 Production de code 5.1 Les objets et leurs adresses . . . . . . . . . . . . 5.1.1 Classes dobjets . . . . . . . . . . . . . . . 5.1.2 Do` u viennent les adresses des objets ? . . 5.1.3 Compilation s epar ee et edition de liens . . 5.2 La machine Mach 1 . . . . . . . . . . . . . . . . . 5.2.1 Machines ` a registres et machines ` a pile . . 5.2.2 Structure g en erale de la machine Mach 1 5.2.3 Jeu dinstructions . . . . . . . . . . . . . 5.2.4 Compl ements sur lappel des fonctions . . 5.3 Exemples de production de code . . . . . . . . . 5.3.1 Expressions arithm etiques . . . . . . . . . 5.3.2 Instruction conditionnelle et boucles . . . 5.3.3 Appel de fonction . . . . . . . . . . . . .

Introduction

Dans le sens le plus usuel du terme, la compilation est une transformation que lon fait subir ` a un programme ecrit dans un langage evolu e pour le rendre ex ecutable. Fondamentalement, cest une traduction : un texte ecrit en Pascal, C, Java, etc., exprime un algorithme et il sagit de produire un autre texte, sp eciant le m eme algorithme dans le langage dune machine que nous cherchons ` a programmer. En g en eralisant un peu, on peut dire que compiler cest lire une suite de caract` eres ob eissant ` a une certaine syntaxe, en construisant une (autre) repr esentation de linformation que ces caract` eres expriment. De ce point de vue, beaucoup dop erations apparaissent comme etant de la compilation ; ` a la limite, la lecture dun nombre, quon obtient en C par une instruction comme : scanf("%f", &x); est d ej` a de la compilation, puisquil sagit de lire des caract` eres constituant l ecriture dune valeur selon la syntaxe des nombres d ecimaux et de fabriquer une autre repr esentation de la m eme information, ` a savoir sa valeur num erique. Bien s ur, les questions qui nous int eresseront ici seront plus complexes que la simple lecture dun nombre. Mais il faut comprendre que le domaine dapplication des principes et m ethodes de l ecriture de compilateurs contient bien dautres choses que la seule production de programmes ex ecutables. Chaque fois que vous aurez a ` ecrire un programme lisant des expressions plus compliqu ees que des nombres vous pourrez tirer prot des concepts, techniques et outils expliqu es dans ce cours.

1.1

Structure de principe dun compilateur

La nature de ce qui sort dun compilateur est tr` es variable. Cela peut etre un programme ex ecutable pour un processeur physique, comme un Pentium III ou un G4, ou un chier de code pour une machine virtuelle, comme la machine Java, ou un code abstrait destin e` a un outil qui en fera ult erieurement du code ex ecutable, ou encore le codage dun arbre repr esentant la structure logique dun programme, etc. En entr ee dun compilateur on trouve toujours la m eme chose : une suite de caract` eres, appel ee le texte source 1 . Voici les phases dans lesquelles se d ecompose le travail dun compilateur, du moins dun point de vue logique2 (voyez la gure 1) : Analyse lexicale Dans cette phase, les caract` eres isol es qui constituent le texte source sont regroup es pour former des unit es lexicales, qui sont les mots du langage. Lanalyse lexicale op` ere sous le contr ole de lanalyse syntaxique ; elle appara t comme une sorte de fonction de lecture am elior ee , qui fournit un mot lors de chaque appel. Analyse syntaxique Alors que lanalyse lexicale reconna t les mots du langage, lanalyse syntaxique en reconna t les phrases. Le r ole principal de cette phase est de dire si le texte source appartient au langage consid er e, cest-` a-dire sil est correct relativement ` a la grammaire de ce dernier. Analyse s emantique La structure du texte source etant correcte, il sagit ici de v erier certaines propri et es s emantiques, cest-` a-dire relatives ` a la signication de la phrase et de ses constituants : les identicateurs apparaissant dans les expressions ont-ils et e declar es ? les op erandes ont-ils les types requis par les op erateurs ? les op erandes sont-ils compatibles ? ny a-t-il pas des conversions ` a ins erer ? les arguments des appels de fonctions ont-ils le nombre et le type requis ? etc. G en eration de code interm ediaire Apr` es les phases danalyse, certains compilateurs ne produisent pas directement le code attendu en sortie, mais une repr esentation interm ediaire, une sorte de code pour une machine abstraite. Cela permet de concevoir ind ependamment les premi` eres phases du compilateur (constituant ce que lon appelle sa face avant ) qui ne d ependent que du langage source compil e et les derni` eres phases (formant sa face arri` ere ) qui ne d ependent que du langage cible ; lid eal serait davoir plusieurs faces avant et plusieurs faces arri` ere quon pourrait assembler librement3 .
1 Conseil : le texte source a probablement et e compos e` a laide dun editeur de textes qui le montre sous forme de pages faites de plusieurs lignes mais, pour ce que nous avons ` a en faire ici, prenez lhabitude de limaginer comme sil etait ecrit sur un long et mince ruban, formant une seule ligne. 2 Cest une organisation logique ; en pratique certaines de ces phases peuvent etre imbriqu ees, et dautres absentes. 3 De la sorte, avec n faces avant pour n langages source et m faces arri` ere correspondant ` a m machines cibles, on disposerait automatiquement de n m compilateurs distincts. Mais cela reste, pour le moment, un fantasme dinformaticien.

pos = posInit + vit * 60 analyse lexicale id(1) aff id(2) add id(3) mul nbr(60) analyse syntaxique aff id(1) id(2) table de symboles ... 1 pos ... 2 posInit ... 3 vit ... ... id(1) id(3) add mul nbr(60)

analyse smantique aff addRelle mulRelle id(2) entVersRel id(3) 60 gnration de code intermdiaire tmp1 tmp2 tmp3 id(1) <te <<<EntVersRel(60) mulRel(id(3), tmp1) addRel(id(2), tmp2) tmp3

optimisation du code tmp1 <- mulRel(id(3), 60.0) id(1) <- addRel(id(2), tmp1) gnration du code final MOVF MULF ADDF MOVF id3, R0 #60.0, R0 id2, R0 R0, id1

Fig. 1 Phases logiques de la compilation dune instruction

Optimisation du code Il sagit g en eralement ici de transformer le code an que le programme r esultant sex ecute plus rapidement. Par exemple d etecter linutilit e de recalculer des expressions dont la valeur est d ej` a connue, a lext erieur des boucles des expressions et sous-expressions dont les op erandes ont la m eme transporter ` valeur ` a toutes les it erations etecter, et supprimer, les expressions inutiles d 4

etc. eophyte, nest pas forc ement la plus G en eration du code nal Cette phase, la plus impressionnante pour le n dicile ` a r ealiser. Elle n ecessite la connaissance de la machine cible (r eelle, virtuelle ou abstraite), et notamment de ses possibilit es en mati` ere de registres, piles, etc.

Analyse lexicale

Lanalyse lexicale est la premi` ere phase de la compilation. Dans le texte source, qui se pr esente comme un ot de caract` eres, lanalyse lexicale reconna t des unit es lexicales, qui sont les mots  avec lesquels les phrases sont form ees, et les pr esente ` a la phase suivante, lanalyse syntaxique. Les principales sortes dunit es lexicales quon trouve dans les langages de programmation courants sont : les caract` eres sp eciaux simples : +, =, etc. les caract` eres sp eciaux doubles : <=, ++, etc. les mots-cl es : if, while, etc. les constantes litt erales : 123, -5, etc. et les identicateurs : i, vitesse_du_vent, etc. A propos dune unit e lexicale reconnue dans le texte source on doit distinguer quatre notions importantes : lunit e lexicale, repr esent ee g en eralement par un code conventionnel ; pour nos dix exemples +, =, <=, ++, if, while, 123, -5, i et vitesse_du_vent, ce pourrait etre, respectivement4 : PLUS, EGAL, INFEGAL, PLUSPLUS, SI, TANTQUE, NOMBRE, NOMBRE, IDENTIF, IDENTIF. le lex` eme, qui est la cha ne de caract` eres correspondante. Pour les dix exemples pr ec edents, les lex` emes correspondants sont : "+", "=", "<=", "++", "if", "while", "123", "-5", "i" et "vitesse_du_vent" eventuellement, un attribut, qui d epend de lunit e lexicale en question, et qui la compl` ete. Seules les derni` eres des dix unit es pr ec edentes ont un attribut ; pour un nombre, il sagit de sa valeur (123, 5) ; pour un identicateur, il sagit dun renvoi ` a une table dans laquelle sont plac es tous les identicateurs rencontr es (on verra cela plus loin). le mod` ele qui sert ` a sp ecier lunit e lexicale. Nous verrons ci-apr` es des moyens formels pour d enir rigoureusement les mod` eles ; pour le moment nous nous contenterons de descriptions informelles comme : pour les caract` eres sp eciaux simples et doubles et les mots r eserv es, le lex` eme et le mod` ele co ncident, le mod` ele dun nombre est une suite de chires, eventuellement pr ec ed ee dun signe , ele dun identicateur est une suite de lettres, de chires et du caract` ere , commen cant par le mod` une lettre . Outre la reconnaissance des unit es lexicales, les analyseurs lexicaux assurent certaines t aches mineures comme la suppression des caract` eres de d ecoration (blancs, tabulations, ns de ligne, etc.) et celle des commentaires (g en eralement consid er es comme ayant la m eme valeur quun blanc), linterface avec les fonctions de lecture de caract` eres, ` a travers lesquelles le texte source est acquis, la gestion des chiers et lachage des erreurs, etc. Remarque. La fronti` ere entre lanalyse lexicale et lanalyse syntaxique nest pas xe. Dailleurs, lanalyse lexicale nest pas une obligation, on peut concevoir des compilateurs dans lesquels la syntaxe est d enie ` a partir des caract` eres individuels. Mais les analyseurs syntaxiques quil faut alors ecrire sont bien plus complexes que ceux quon obtient en utilisant des analyseurs lexicaux pour reconna tre les mots du langage. Simplicit e et ecacit e sont les raisons d etre des analyseurs lexicaux. Comme nous allons le voir, les techniques pour reconna tre les unit es lexicales sont bien plus simples et ecaces que les techniques pour v erier la syntaxe.

2.1
2.1.1

Expressions r eguli` eres


D enitions

Les expressions r eguli` eres sont une importante notation pour sp ecier formellement des mod` eles. Pour les d enir correctement il nous faut faire leort dapprendre un peu de vocabulaire nouveau : Un alphabet est un ensemble de symboles. Exemples : {0, 1}, {A, C, G, T }, lensemble de toutes les lettres, lensemble des chires, le code ASCII, etc. On notera que les caract` eres blancs (cest-` a-dire les espaces, les
4 Dans

un analyseur lexical ecrit en C, ces codes sont des pseudo-constantes introduites par des directives #define

tabulations et les marques de n de ligne) ne font g en eralement pas partie des alphabets5 . Une cha ne (on dit aussi mot ) sur un alphabet est une s equence nie de symboles de . Exemples, respectivement relatifs aux alphabets pr ec edents : 00011011, ACCAGTTGAAGTGGACCTTT, Bonjour, 2001. On note la cha ne vide, ne comportant aucun caract` ere. Un langage sur un alphabet est un ensemble de cha nes construites sur . Exemples triviaux : , le langage vide, {}, le langage r eduit ` a lunique cha ne vide. Des exemples plus int eressants (relatifs aux alphabets pr ec edents) : lensemble des nombres en notation binaire, lensemble des cha nes ADN, lensemble des mots de la langue fran caise, etc. Si x et y sont deux cha nes, la concat enation de x et y , not ee xy , est la cha ne obtenue en ecrivant y imm ediatement apr` es x. Par exemple, la concat enation des cha nes anti et moine est la cha ne antimoine. Si x est une cha ne, on d enit x0 = et, pour n > 0, xn = xn1 x = xxn1 . On a donc x1 = x, x2 = xx, x3 = xxx, etc. Les op erations sur les langages suivantes nous serviront ` a d enir les expressions r eguli` eres. Soient L et M deux langages, on d enit : d enomination lunion de L et M la concat enation de L et M la fermeture de Kleene de L la fermeture positive de L notation LM LM L L+ d enition { x | x L ou x M } { xy | x L et y M } { x1 x2 ...xn | xi L, n N et n 0} { x1 x2 ...xn | xi L, n N et n > 0}

De la d enition de LM on d eduit celle de Ln = LL . . . L. Exemples. On se donne les alphabets L = {A, B, . . . Z, a, b, . . . z }, ensemble des lettres, et C = {0, 1, . . . 9}, ensemble des chires. En consid erant quun caract` ere est la m eme chose quune cha ne de longueur un, on peut voir L et C comme des langages, form es de cha nes de longueur un. Dans ces conditions : L C est lensemble des lettres et des chires, LC est lensemble des cha nes form ees dune lettre suivie dun chire, L4 est lensemble des cha nes de quatre lettres, L est lensemble des cha nes faites dun nombre quelconque de lettres ; en fait partie, C + est lensemble des cha nes de chires comportant au moins un chire, L(L C ) est lensemble des cha nes de lettres et chires commen cant par une lettre. gulie `re. Soit un alphabet. Une expression r Expression re eguli` ere r sur est une formule qui d enit un langage L(r) sur , de la mani` ere suivante : 1. est une expression r eguli` ere qui d enit le langage {} 2. Si a , alors a est une expression r eguli` ere qui d enit le langage6 {a} 3. Soient x et y deux expressions r eguli` eres, d enissant les langages L(x) et L(y ). Alors (x)|(y ) est une expression r eguli` ere d enissant le langage L(x) L(y ) (x)(y ) est une expression r eguli` ere d enissant le langage L(x)L(y ) (x) est une expression r eguli` ere d enissant le langage (L(x)) (x) est une expression r eguli` ere d enissant le langage L(x) La derni` ere r` egle ci-dessus signie quon peut encadrer une expression r eguli` ere par des parenth` eses sans changer le langage d eni. Dautre part, les parenth` eses apparaissant dans les r` egles pr ec edentes peuvent souvent etre omises, en fonction des op erateurs en pr esence : il sut de savoir que les op erateurs , concat enation et | sont associatifs ` a gauche, et v erient priorit e ( ) > priorit e ( concat enation ) > priorit e( | ) Ainsi, on peut ecrire lexpression r eguli` ere oui au lieu de (o)(u)(i) et oui|non au lieu de (oui)|(non), mais on ne doit pas ecrire oui au lieu de (oui) .
5 Il en d ecoule que les unit es lexicales, sauf mesures particuli` eres (apostrophes, quillemets, etc.), ne peuvent pas contenir des caract` eres blancs. Dautre part, la plupart des langages autorisent les caract` eres blancs entre les unit es lexicales. 6 On prendra garde ` a labus de langage quon fait ici, en employant la m eme notation pour le caract` ere a, la cha ne a et lexpression r eguli` ere a. En principe, le contexte permet de savoir de quoi on parle.

finitions re gulie `res. Les expressions r De eguli` eres se construisent ` a partir dautres expressions r eguli` eres ; cela am` ene ` a des expressions passablement touues. On les all` ege en introduisant des d enitions r eguli` eres qui permettent de donner des noms ` a certaines expressions en vue de leur r eutilisation. On ecrit donc d1 r1 d2 r2 ... dn rn o` u chaque di est une cha ne sur un alphabet disjoint de 7 , distincte de d1 , d2 , . . . di1 , et chaque ri une expression r eguli` ere sur {d1 , d2 , . . . di1 }. Exemple. Voici quelques d enitions r eguli` eres, et notamment celles de identicateur et nombre, qui d enissent les identicateurs et les nombres du langage Pascal : lettre A | B | . . . | Z | a | b | . . . | z chire 0 | 1 | . . . | 9 identicateur lettre ( lettre | chire ) chires chire chire fraction-opt . chires | exposant-opt ( E (+ | - | ) chires ) | nombre chires fraction-opt exposant-opt ge es. Pour all Notations abre eger certaines ecritures, on compl` ete la d enition des expressions r eguli` eres en ajoutant les notations suivantes : soit x une expression r eguli` ere, d enissant le langage L(x) ; alors (x)+ est une expression r eguli` ere, qui + d enit le langage (L(x)) , soit x une expression r eguli` ere, d enissant le langage L(x) ; alors (x)? est une expression r eguli` ere, qui d enit le langage L(x) { }, si c1 , c2 , . . . ck sont des caract` eres, lexpressions r eguli` ere c1 |c2 | . . . |ck peut se noter [c1 c2 . . . ck ], ` a lint erieur dune paire de crochets comme ci-dessus, lexpression c1 c2 d esigne la s equence de tous les caract` eres c tels que c1 c c2 . Les d enitions de lettre et chire donn ees ci-dessus peuvent donc se r e ecrire : lettre [AZaz] chire [09] 2.1.2 Ce que les expressions r eguli` eres ne savent pas faire

Les expressions r eguli` eres sont un outil puissant et pratique pour d enir les unit es lexicales, cest-` a-dire les constituants el ementaires des programmes. Mais elles se pr etent beaucoup moins bien ` a la sp ecication de constructions de niveau plus elev e, car elles deviennent rapidement dune trop grande complexit e. De plus, on d emontre quil y a des cha nes quon ne peut pas d ecrire par des expressions r eguli` eres. Par exemple, le langage suivant (suppos e inni) { a, (a), ((a)), (((a))), . . . } ne peut pas etre d eni par des expressions r eguli` eres, car ces derni` eres ne permettent pas dassurer quil y a dans une expression de la forme (( . . . ((a)) . . . )) autant de parenth` eses ouvrantes que de parenth` eses fermantes. On dit que les expressions r eguli` eres ne savent pas compter . Pour sp ecier ces structures equilibr ees, si importantes dans les langages de programmation (penser aux parenth` eses dans les expressions arithm etiques, les crochets dans les tableaux, begin...end, {...}, if...then..., etc.) nous ferons appel aux grammaires non contextuelles, expliqu ees ` a la section 3.1.
7 On

assure souvent la s eparation entre et les noms des d enitions r eguli` eres par des conventions typographiques.

2.2

Reconnaissance des unit es lexicales

Nous avons vu comment sp ecier les unit es lexicales ; notre probl` eme maintenant est d ecrire un programme qui les reconna t dans le texte source. Un tel programme sappelle un analyseur lexical. Dans un compilateur, le principal client de lanalyseur lexical est lanalyseur syntaxique. Linterface entre ces deux analyseurs est une fonction int uniteSuivante(void) 8 , qui renvoie ` a chaque appel lunit e lexicale suivante trouv ee dans le texte source. Cela suppose que lanalyseur lexical et lanalyseur syntaxique partagent les d enitions des constantes conventionnelles d enissant les unit es lexicales. Si on programme en C, cela veut dire que dans les chiers sources des deux analyseurs on a inclus un chier dent ete (chier .h ) comportant une s erie de d enitions comme9 : #define #define #define #define #define etc. IDENTIF NOMBRE SI ALORS SINON 1 2 3 4 5

Cela suppose aussi que lanalyseur lexical et lanalyseur syntaxique partagent egalement une variable globale contenant le lex` eme correspondant ` a la derni` ere unit e lexicale reconnue, ainsi quune variable globale contenant le (ou les) attribut(s) de lunit e lexicale courante, lorsque cela est pertinent, et notamment lorsque lunit e lexicale est NOMBRE ou IDENTIF. On se donnera, du moins dans le cadre de ce cours, quelques r` egles du jeu  suppl ementaires : lanalyseur lexical est glouton  : chaque lex` eme est le plus long possible10 ; ede au texte source. Lanalyseur syntaxique nacquiert ses donn ees dentr ee seul lanalyseur lexical acc` autrement qu` a travers la fonction uniteSuivante ; lanalyseur lexical acquiert le texte source un caract` ere ` a la fois. Cela est un choix que nous faisons ici ; dautres choix auraient et e possibles, mais nous verrons que les langages qui nous int eressent permettent de travailler de cette mani` ere. 2.2.1 Diagrammes de transition

Pour illustrer cette section nous allons nous donner comme exemple le probl` eme de la reconnaissance des unit es lexicales INFEG, DIFF, INF, EGAL, SUPEG, SUP, IDENTIF, respectivement d enies par les expressions r eguli` eres <=, <>, <, =, >=, > et lettre (lettre |chire ) , lettre et chire ayant leurs d enitions d ej` a vues. Les diagrammes de transition sont une etape pr eparatoire pour la r ealisation dun analyseur lexical. Au fur et ` a mesure quil reconna t une unit e lexicale, lanalyseur lexical passe par divers etats. Ces etats sont num erot es et repr esent es dans le diagramme par des cercles. De chaque etat e sont issues une ou plusieurs ` eches etiquet ees par des caract` eres. Une ` eche etiquet ee par c relie e ` a l etat e1 dans lequel lanalyseur passera si, alors quil se trouve dans l etat e, le caract` ere c est lu dans le texte source. Un etat particulier repr esente l etat initial de lanalyseur ; on le signale en en faisant lextr emit e dune ` eche etiquet ee debut. Des doubles cercles identient les etats naux, correspondant ` a la reconnaissance compl` ete dune unit e lexicale. Certains etats naux sont marqu es dune etoile : cela signie que la reconnaissance sest faite au prix de la lecture dun caract` ere au-del` a de la n du lex` eme11 . Par exemple, la gure 2 montre les diagrammes traduisant la reconnaissance des unit es lexicales INFEG, DIFF, INF, EGAL, SUPEG, SUP et IDENTIF. Un diagramme de transition est dit non d eterministe lorsquil existe, issues dun m eme etat, plusieurs ` eches etiquet ees par le m eme caract` ere, ou bien lorsquil existe des ` eches etiquet ees par la cha ne vide . Dans le cas
on a employ e loutil lex pour fabriquer lanalyser lexical, cette fonction sappelle plut ot yylex ; le lex` eme est alors point e par la variable globale yytext et sa longueur est donn ee par la variable globale yylen. Tout cela est expliqu e` a la section 2.3. 9 Peu importent les valeurs num eriques utilis ees, ce qui compte est quelles soient distinctes. 10 Cette r` egle est peu mentionn ee dans la litt erature, pourtant elle est fondamentale. Cest gr ace ` a elle que 123 est reconnu comme un nombre, et non plusieurs, vitesseV ent comme un seul identicateur, et f orce comme un identicateur, et non pas comme un mot r eserv e suivi dun identicateur. 11 Il faut etre attentif ` a ce caract` ere, car il est susceptible de faire partie de lunit e lexicale suivante, surtout sil nest pas blanc.
8 Si

dbut

<

= >
autre

2 3 4
*

rendre INFEG rendre DIFF rendre INF

= >

5 rendre EGAL 6 =
autre lettre

7 8
*

rendre SUPEG rendre SUP

lettre

9
chiffre

autre

10

* rendre IDENTIF

Fig. 2 Diagramme de transition pour les op erateurs de comparaison et les identicateurs

contraire, le diagramme est dit d eterministe. Il est clair que le diagramme de la gure 2 est d eterministe. Seuls les diagrammes d eterministes nous int eresseront dans le cadre de ce cours. 2.2.2 Analyseurs lexicaux programm es en dur 

Les diagrammes de transition sont une aide importante pour l ecriture danalyseurs lexicaux. Par exemple, ` partir du diagramme de la gure 2 on peut obtenir rapidement un analyseur lexical reconnaissant les unit a es INFEG, DIFF, INF, EGAL, SUPEG, SUP et IDENTIF. Auparavant, nous apportons une l eg` ere modication ` a nos diagrammes de transition, an de permettre que les unit es lexicales soient s epar ees par un ou plusieurs blancs12 . La gure 3 montre le (d ebut du) diagramme modi e13 .
blanc dbut < = > lettre

... ... ... ...

Fig. 3 Ignorer les blancs devant une unit e lexicale

Et voici le programme obtenu : int uniteSuivante(void) { char c; c = lireCar();


12 Nous 13 Notez

/* etat = 0 */

etc.

appelons blanc  une espace, un caract` ere de tabulation ou une marque de n de ligne que cela revient ` a modier toutes les expressions r eguli` eres, en rempla cant <=  par (blanc ) <= , <  par (blanc ) < ,

while (estBlanc(c)) c = lireCar(); if (c == <) { c = lireCar(); /* if (c == =) return INFEG; /* else if (c == >) return DIFF; /* else { delireCar(c); /* return INF; } } else if (c == =) return EGAL; /* else if (c == >) { c = lireCar(); /* if (c == =) return SUPEG; /* else { delireCar(c); /* return SUP; } } else if (estLettre(c)) { lonLex = 0; /* lexeme[lonLex++] = c; c = lireCar(); while (estLettre(c) || estChiffre(c)) { lexeme[lonLex++] = c; c = lireCar(); } delireCar(c); /* return IDENTIF; } else { delireCar(c); return NEANT; /* ou bien donner une erreur } }

etat = 1 */ etat = 2 */ etat = 3 */ etat = 4 */

etat = 5 */ etat = 6 */ etat = 7 */ etat = 8 */

etat = 9 */

etat = 10 */

*/

Dans le programme pr ec edent on utilise des fonctions auxiliaires, dont voici une version simple : int estBlanc(char c) { return c == || c == \t || c == \n; } int estLettre(char c) { return A <= c && c <= Z || a <= c && c <= z; } int estChiffre(char c) { return 0 <= c && c <= 9; } Note. On peut augmenter lecacit e de ces fonctions, au d etriment de leur s ecurit e dutilisation, en en faisant des macros : #define estBlanc(c) ((c) == || (c) == \t || (c) == \n) #define estLettre(c) (A <= (c) && (c) <= Z || a <= (c) && (c) <= z) #define estChiffre(c) (0 <= (c) && (c) <= 9) 10

Il y plusieurs mani` eres de prendre en charge la restitution dun caract` ere lu en trop (notre fonction delireCar ). Si on dispose de la biblioth` eque standard C on peut utiliser la fonction ungetc : void delireCar(char c) { ungetc(c, stdin); } char lireCar(void) { return getc(stdin); } Une autre mani` ere de faire permet de se passer de la fonction ungetc. Pour cela, on g` ere une variable globale contenant, quand il y a lieu, le caract` ere lu en trop (il ny a jamais plus dun caract` ere lu en trop). D eclaration : int carEnAvance = -1; avec cela nos deux fonctions deviennent void delireCar(char c) { carEnAvance = c; } char lireCar(void) { char r; if (carEnAvance >= 0) { r = carEnAvance; carEnAvance = -1; } else r = getc(stdin); return r; } serve s. Les mots r Reconnaissance des mots re eserv es appartiennent au langage d eni par lexpression r eguli` ere lettre (lettre |chire ) , tout comme les identicateurs. Leur reconnaissance peut donc se traiter de deux mani` eres : soit on incorpore les mots r eserv es au diagrammes de transition, ce qui permet dobtenir un analyseur tr` es ecace, mais au prix dun travail de programmation plus important, car les diagrammes de transition deviennent tr` es volumineux14 , soit on laisse lanalyseur traiter de la m eme mani` ere les mots r eserv es et les identicateurs puis, quand la reconnaissance dun identicateur-ou-mot-r eserv e  est termin ee, on recherche le lex` eme dans une table pour d eterminer sil sagit dun identicateur ou dun mot r eserv e. Dans les analyseurs lexicaux en dur  on utilise souvent la deuxi` eme m ethode, plus facile ` a programmer. On se donne donc une table de mots r eserv es : struct { char *lexeme; int uniteLexicale; } motRes[] = { { "si", SI }, { "alors", ALORS }, { "sinon", SINON }, ... }; int nbMotRes = sizeof motRes / sizeof motRes[0]; puis on modie de la mani` ere suivante la partie concernant les identicateurs de la fonction uniteSuivante : ... else if (estLettre(c)) { lonLex = 0; lexeme[lonLex++] = c;
14 Nous

/* etat =

9 */

utiliserons cette solution quand nous etudierons loutil lex, ` a la section 2.3

11

c = lireCar(); while (estLettre(c) || estChiffre(c)) { lexeme[lonLex++] = c; c = lireCar(); } delireCar(c); /* etat = 10 */ lexeme[lonLex] = \0; for (i = 0; i < nbMotRes; i++) if (strcmp(lexeme, motRes[i].lexeme) == 0) return motRes[i].uniteLexicale; return IDENTIF; } ... 2.2.3 Automates nis

Un automate ni est d eni par la donn ee de un ensemble ni d etats E , un ensemble ni de symboles (ou alphabet ) dentr ee , une fonction de transition, transit : E E , un etat 0 distingu e, appel e etat initial, un ensemble d etats F , appel es etats dacceptation ou etats naux. Un automate peut etre repr esent e graphiquement par un graphe o` u les etats sont gur es par des cercles (les etats naux par des cercles doubles) et la fonction de transition par des ` eches etiquet ees par des caract` eres : si transit(e1 , c) = e2 alors le graphe a une ` eche etiquet ee par le caract` ere c, issue de e1 et aboutissant ` a e2 . Un tel graphe est exactement ce que nous avons appel e diagramme de transition ` a la section 2.2.1 (voir la gure 2). Si on en reparle ici cest quon peut en d eduire un autre style danalyseur lexical, assez di erent de ce que nous avons appel e analyseur programm e en dur . On dit quun automate ni accepte une cha ne dentr ee s = c1 c2 . . . ck si et seulement si il existe dans le graphe de transition un chemin joignant l etat initial e0 ` a un certain etat nal ek , compos e de k ` eches etiquet ees par les caract` eres c1 , c2 , . . . ck . Pour transformer un automate ni en un analyseur lexical il sura donc dassocier une unit e lexicale ` a chaque etat nal et de faire en sorte que lacceptation dune cha ne produise comme r esultat lunit e lexicale associ ee ` a l etat nal en question. Autrement dit, pour programmer un analyseur il sura maintenant dimpl ementer la fonction transit ce qui, puisquelle est d enie sur des ensembles nis, pourra se faire par une table ` a double entr ee. Pour les diagrammes des gures 2 et 3 cela donne la table suivante (les etats naux sont indiqu es en gras, leurs lignes ont et e supprim ees) : 0 4 8 10 \t 0 4 8 10 \n 0 4 8 10 < 1 4 8 10 = 5 2 7 10 > 6 3 8 10 lettre 9 4 8 9 chire erreur 4 8 9 autre erreur 4 8 10

0 1 6 9

On obtiendra un analyseur peut- etre plus encombrant que dans la premi` ere mani` ere, mais certainement plus rapide puisque lessentiel du travail de lanalyseur se r eduira ` a r ep eter b etement  laction etat = transit[etat][lireCar( jusqu` a tomber sur un etat nal. Voici ce programme : #define NBR_ETATS ... #define NBR_CARS 256 int transit[NBR_ETATS][NBR_CARS];

12

int final[NBR_ETATS + 1]; int uniteSuivante(void) { char caractere; int etat = etatInitial; while ( ! final[etat]) { caractere = lireCar(); etat = transit[etat][caractere]; } if (final[etat] < 0) delireCar(caractere); return abs(final[etat]) - 1; } Notre tableau final, index e par les etats, est d eni par final[e] = 0 si e nest pas un etat nal (vu comme un bool een, final[e] est faux), final[e] = U + 1 si e est nal, sans etoile et associ e` a lunit e lexicale U (en tant que bool een, final[e] est vrai, car les unit es lexicales sont num erot ees au moins ` a partir de z ero), final[e] = (U + 1) si e est nal, etoil e et associ e` a lunit e lexicale U (en tant que bool een, final[e] est encore vrai). Enn, voici comment les tableaux transit et final devraient etre initialis es pour correspondre aux diagrammes des gures 2 et 315 : void initialiser(void) { int i, j; for (i = 0; i < NBR_ETATS; i++) final[i] = 0; final[ 2] = INFEG + 1; final[ 3] = DIFF + 1; final[ 4] = - (INF + 1); final[ 5] = EGAL + 1; final[ 7] = SUPEG + 1; final[ 8] = - (SUP + 1); final[10] = - (IDENTIF + 1); final[NBR_ETATS] = ERREUR + 1; for (i = 0; i < NBR_ETATS; i++) for (j = 0; j < NBR_CARS; j++) transit[i][j] = NBR_ETATS; transit[0][ ] = 0; transit[0][\t] = 0; transit[0][\n] = 0; transit[0][<] = 1; transit[0][=] = 5; transit[0][>] = 6; for (j = A; j <= Z; j++) transit[0][j] = 9; for (j = a; j <= z; j++) transit[0][j] = 9; for (j = 0; j < NBR_CARS; j++) transit[1][j] = 4; transit[1][=] = 2; transit[1][>] = 3;
15 Nous avons ajout e un etat suppl ementaire, ayant le num ero NBR ETATS, qui correspond ` a la mise en erreur de lanalyseur lexical, et une unit e lexicale ERREUR pour signaler cela

13

for (j = 0; j < NBR_CARS; j++) transit[6][j] = 8; transit[6][=] = 7; for for for for } (j (j (j (j = = = = 0; j A; a; 0; < j j j NBR_CARS; j++) transit[9][j] <= Z; j++) transit[9][j] = <= z; j++) transit[9][j] = <= 9; j++) transit[9][j] = = 10; 9; 9; 9;

2.3

Lex, un g en erateur danalyseurs lexicaux

Les analyseurs lexicaux bas es sur des tables de transitions sont les plus ecaces... une fois la table de transition construite. Or, la construction de cette table est une op eration longue et d elicate. Le programme lex 16 fait cette construction automatiquement : il prend en entr ee un ensemble dexpressions r eguli` eres et produit en sortie le texte source dun programme C qui, une fois compil e, est lanalyseur lexical correspondant au langage d eni par les expressions r eguli` eres en question. Plus pr ecis ement (voyez la gure 4), lex produit un chier source C, nomm e lex.yy.c, contenant la d enition de la fonction int yytext(void), qui est lexacte homologue de notre fonction uniteSuivante de la section 2.2.2 : un programme appelle cette fonction et elle renvoie une unit e lexicale reconnue dans le texte source.

expressions rgulires

lex

lex.yy.c

lex.yy.c gcc autres modules monProg

source analyser

monProg

rsultat de l'analyse

Fig. 4 Utilisation de lex

2.3.1

Structure dun chier source pour lex

En lisant cette section, souvenez-vous de ceci : lex ecrit un chier source C. Ce chier est fait de trois sortes dingr edients : des tables garnies de valeurs calcul ees ` a partir des expressions r eguli` eres fournies, a-dire la boucle qui des morceaux de code C invariable, et notamment le moteur  de lautomate, cest-` r ep` ete inlassablement etat transit (etat, caractere), es dans le chier source lex et recopi es tels quels, ` a lendroit voulu, dans le des morceaux de code C, trouv chier produit. Un chier source pour lex doit avoir un nom qui se termine par .l . Il est fait de trois sections, d elimit ees par deux lignes r eduites17 au symbole %% : %{ d eclarations pour le compilateur C %} d enitions r eguli` eres
est un programme gratuit quon trouve dans le syst` eme UNIX pratiquement depuis ses d ebuts. De nos jours on utilise souvent ex, une version am elior ee de lex qui appartient ` a la famille GNU. 17 Notez que les symboles %%, %{ et %}, quand ils apparaissent, sont ecrits au d ebut de la ligne, aucun blanc ne les pr ec` ede.
16 Lex

14

%% r` egles %% fonctions C suppl ementaires La partie d eclarations pour le compilateur C  et les symboles %{ et %} qui lencadrent peuvent etre omis. Quand elle est pr esente, cette partie se compose de d eclarations qui seront simplement recopi ees au d ebut du chier produit. En plus dautres choses, on trouve souvent ici une directive #include qui produit linclusion du chier .h  contenant les d enitions des codes conventionnels des unit es lexicales (INFEG, INF, EGAL, etc.). La troisi` eme section fonctions C suppl ementaires  peut etre absente egalement (le symbole %% qui la s epare de la deuxi` eme section peut alors etre omis). Cette section se compose de fonctions C qui seront simplement recopi ees ` a la n du chier produit. finitions re gulie `res. Les d De enitions r eguli` eres sont de la forme identicateur expressionR eguli` ere o` u identicateur est ecrit au d ebut de la ligne (pas de blancs avant) et s epar e de expressionR eguli` ere par des blancs. Exemples : lettre chiffre [A-Za-z] [0-9]

Les identicateurs ainsi d enis peuvent etre utilis es dans les r` egles et dans les d enitions subs equentes ; il faut alors les encadrer par des accolades. Exemples : lettre chiffre alphanum %% {lettre}{alphanum}* {chiffre}+("."{chiffre}+)? expressionR eguli` ere { return IDENTIF; } { return NOMBRE; } [A-Za-z] [0-9] {lettre}|{chiffre}

`gles. Les r` Re egles sont de la forme { action } o` u expressionR eguli` ere est ecrit au d ebut de la ligne (pas de blancs avant) ; action est un morceau de code source C, qui sera recopi e tel quel, au bon endroit, dans la fonction yylex. Exemples : if then else ... {lettre}{alphanum}* { return SI; } { return ALORS; } { return SINON; } { return IDENTIF; }

La r` egle expressionR eguli` ere { action } signie ` a la n de la reconnaissance dune cha ne du langage d eni par expressionR eguli` ere ex ecutez action . Le traitement par lex dune telle r` egle consiste donc ` a recopier laction indiqu ee ` a un certain endroit de la fonction yylex 18 . Dans les exemples ci-dessus, les actions etant toutes de la forme return unite , leur signication est claire : quand une cha ne du texte source est reconnue, la fonction yylex se termine en rendant comme r esultat lunit e lexicale reconnue. Il faudra appeler de nouveau cette fonction pour que lanalyse du texte source reprenne. A la n de la reconnaissance dune unit e lexicale la cha ne accept ee est la valeur de la variable yytext, de type cha ne de caract` eres19 . Un caract` ere nul indique la n de cette cha ne ; de plus, la variable enti` ere yylen donne le nombre de ses caract` eres. Par exemple, la r` egle suivante reconna t les nombres entiers et en calcule la valeur dans une variable yylval :
parce que ces actions sont copi ees dans une fonction quon a le droit dy utiliser linstruction return. variable yytext est d eclar ee dans le chier produit par lex ; il vaut mieux ne pas chercher ` a y faire r ef erence dans dautres chiers, car il nest pas sp eci e laquelle des d eclarations extern char *yytext  ou extern char yytext[]  est pertinente.
19 La 18 Cest

15

(+|-)?[0-9]+

{ yylval = atoi(yytext); return NOMBRE; }

gulie `res. Les expressions r Expressions re eguli` eres accept ees par lex sont une extension de celles d enies a la section 2.1. Les m ` eta-caract` eres pr ec edemment introduits, cest-` a-dire (, ), |, , +, ?, [, ] et ` a lint erieur des crochets, sont l egitimes dans lex et y ont le m eme sens. En outre, on dispose de ceci (liste non exhaustive) : un point . signie un caract` ere quelconque, di erent de la marque de n de ligne, on peut encadrer par des guillemets un caract` ere ou une cha ne, pour eviter que les m eta-caract` eres qui sy trouvent soient interpr et es comme tels. Par exemple, "." signie le caract` ere . (et non pas un caract` ere quelconque), " " signie un blanc, "[a-z]" signie la cha ne [a-z], etc., Dautre part, on peut sans inconv enient encadrer par des guillemets un caract` ere ou une cha ne qui nen avaient pas besoin, lexpression [^caract` eres ] signie : tout caract` ere nappartenant pas ` a lensemble d eni par [caract` eres ], lexpression ^expressionR eguli` ere  signie : toute cha ne reconnue par expressionR eguli` ere ` a la condition quelle soit au d ebut dune ligne, lexpression expressionR eguli` ere $  signie : toute cha ne reconnue par expressionR eguli` ere ` a la condition quelle soit ` a la n dune ligne. Attention. Il faut etre tr` es soigneux en ecrivant les d enitions et les r` egles dans le chier source lex. En eet, tout texte qui nest pas exactement ` a sa place (par exemple une d enition ou une r` egle qui ne commencent pas au d ebut de la ligne) sera recopi e dans le chier produit par lex. Cest un comportement voulu, parfois utile, mais qui peut conduire ` a des situations confuses. . Lanalyseur lexical produit par lex prend son texte source sur lentr Echo du texte analyse ee standard20 et l ecrit, avec certaines modications, sur la sortie standard. Plus pr ecisement : tous les caract` eres qui ne font partie daucune cha ne reconnue sont recopi es sur la sortie standard (ils traversent  lanalyseur lexical sans en etre aect es), une cha ne accept ee au titre dune expression r eguli` ere nest pas recopi ee sur la sortie standard. Bien entendu, pour avoir les cha nes accept ees dans le texte ecrit par lanalyseur il sut de le pr evoir dans laction correspondante. Par exemple, la r` egle suivante reconna t les identicateurs et fait en sorte quils gurent dans le texte sorti : [A-Za-z][A-Za-z0-9]* [A-Za-z][A-Za-z0-9]* 2.3.2 Un exemple complet { printf("%s", yytext); return IDENTIF; } { ECHO; return IDENTIF; } Le texte printf("%s", yytext)  appara t tr` es fr equemment dans les actions. On peut labr eger en ECHO :

Voici le texte source pour cr eer lanalyseur lexical dun langage comportant les nombres et les identicateurs d enis comme dhabitude, les mots r eserv es si, alors, sinon, tantque, faire et rendre et les op erateurs doubles ==, !=, <= et >=. Les unit es lexicales correspondantes sont respectivement repr esent ees par les constantes conventionnelles IDENTIF, NOMBRE, SI, ALORS, SINON, TANTQUE, FAIRE, RENDRE, EGAL, DIFF, INFEG, SUPEG, d enies dans le chier unitesLexicales.h. Pour les op erateurs simples on d ecide que tout caract` ere non reconnu par une autre expression r eguli` ere est une unit e lexicale, et quelle est repr esent ee par son propre code ASCII21 . Pour eviter des collisions entre ces codes ASCII et les unit es lexicales nomm ees, on donne ` a ces derni` eres des valeurs sup erieures ` a 255. Fichier unitesLexicales.h : #define #define #define #define #define #define IDENTIF NOMBRE SI ALORS SINON TANTQUE 256 257 258 259 260 261

20 On peut changer ce comportement par d efaut en donnant une valeur ` a la variable yyin, avant le premier appel de yylex ; par exemple : yyin = fopen(argv[1], "r") ; 21 Cela veut dire quon sen remet au client  de lanalyseur lexical, cest-` a-dire lanalyseur syntaxique, pour s eparer les op erateurs pr evus par le langage des caract` eres sp eciaux qui nen sont pas. Ou, dit autrement, que nous transformons lerreur caract` ere ill egal , ` a priori lexicale, en une erreur syntaxique.

16

#define #define #define #define #define #define

FAIRE RENDRE EGAL DIFF INFEG SUPEG

262 263 264 265 266 267

extern int valNombre; extern char valIdentif[]; Fichier analex.l (le source pour lex ) : %{ #include "unitesLexicales.h" %} chiffre [0-9] lettre [A-Za-z] %% [" "\t\n] { ECHO; /* rien */ } {chiffre}+ { ECHO; valNombre = atoi(yytext); return NOMBRE; }; si { ECHO; return SI; } alors { ECHO; return ALORS; } sinon { ECHO; return SINON; } tantque { ECHO; return TANTQUE; } fairerendre { ECHO; return FAIRE; } {lettre}({lettre}|{chiffre})* { ECHO; strcpy(valIdentif, yytext); return IDENTIF; } "==" { ECHO; return EGAL; } "!=" { ECHO; return DIFF; } "<=" { ECHO; return INFEG; } ">=" { ECHO; return SUPEG; } . { ECHO; return yytext[0]; } %% int valNombre; char valIdentif[256]; int yywrap(void) { return 1; } Fichier principal.c (purement d emonstratif) : #include <stdio.h> #include "unitesLexicales.h" int main(void) { int unite; do { unite = yylex(); printf(" (unite: %d", unite); if (unite == NOMBRE) printf(" val: %d", valNombre); else if (unite == IDENTIF) printf(" %s", valIdentif); printf(")\n"); 17

} while (unite != 0); return 0; } Fichier essai.txt pour essayer lanalyseur : si x == 123 alors y = 0; Cr eation dun ex ecutable et essai sur le texte pr ec edent (rappelons que lex sappelle ex dans le monde Linux) ; $ est le prompt du syst` eme : $ flex analex.l $ gcc lex.yy.c principal.c -o monprog $ monprog < essai.txt si (unite: 258) x (unite: 256 x) == (unite: 264) 123 (unite: 257 val: 123) alors (unite: 259) y (unite: 256 y) = (unite: 61) 0 (unite: 257 0) ; (unite: 59) $ Note. La fonction yywrap qui appara t dans notre chier source pour lex est appel ee lorsque lanalyseur rencontre la n du chier ` a analyser22 . Outre d eventuelles actions utiles dans telle ou telle application particuli` ere, cette fonction doit rendre une valeur non nulle pour indiquer que le ot dentr ee est d enitivement epuis e, ou bien ouvrir un autre ot dentr ee. 2.3.3 Autres utilisations de lex

Ce que le programme g en er e par lex fait n ecessairement, cest reconna tre les cha nes du langage d eni par les expressions r eguli` eres donn ees. Quand une telle reconnaissance est accomplie, on nest pas oblig e de renvoyer une unit e lexicale pour signaler la chose ; on peut notamment d eclencher laction quon veut et ne pas retourner ` a la fonction appelante. Cela permet dutiliser lex pour eectuer dautres sortes de programmes que des analyseurs lexicaux. Par exemple, supposons que nous disposons de textes contenant des indications de prix en francs. Voici comment obtenir rapidement un programme qui met tous ces prix en euros : %% [0-9]+("."[0-9]*)?[" "\t\n]*F(rancs|".")?[" "\t\n] { printf("%.2f EUR ", atof(yytext) / 6.55957); } %% int yywrap(void) { return 1; } int main(void) { yylex(); } Le programme pr ec edent exploite le fait que tous les caract` eres qui ne font pas partie dune cha ne reconnue sont recopi es sur la sortie ; ainsi, la plupart des caract` eres du texte donn e seront recopi es tels quels. Les cha nes reconnues sont d enies par lexpression r eguli` ere [0-9]+("."[0-9]*)?[" "\t\n]*F(rancs|".")?[" "\t\n] cette expression se lit, successivement :
22 Dans certaines versions de lex une version simple de la fonction yywrap, r eduite ` a { return 1 ; }, est fournie et on na pas ` a sen occuper.

18

une suite dau moins un chire, eventuellement, un point suivi dun nombre quelconque de chires, eventuellement, un nombre quelconque de blancs (espaces, tabulations, ns de ligne), un F majuscule obligatoire, eventuellement, la cha ne rancs ou un point (ainsi, F , F.  et Francs  sont tous trois accept es), enn, un blanc obligatoire. Lorsquune cha ne saccordant ` a cette syntaxe est reconnue, comme 99.50 Francs , la fonction atof obtient la valeur que [le d ebut de] cette cha ne repr esente. Il sut alors de mettre dans le texte de sortie le r esultat de la division de cette valeur par le taux ad equat ; soit, ici, 15.17 EUR .

3
3.1

Analyse syntaxique
Grammaires non contextuelles

Les langages de programmation sont souvent d enis par des r` egles r ecursives, comme : on a une expression en ecrivant successivement un terme, + et une expression  ou on obtient une instruction en ecrivant ` a la suite si, une expression, alors, une instruction et, eventuellement, sinon et une instruction . Les grammaires non contextuelles sont un formalisme particuli` erement bien adapt e` a la description de telles r` egles. 3.1.1 D enitions

Une grammaire non contextuelle, on dit parfois grammaire BNF (pour Backus-Naur form23 ), est un quadruplet G = (VT , VN , S0 , P ) form e de un ensemble VT de symboles terminaux, un ensemble VN de symboles non terminaux, un symbole S0 VN particulier, appel e symbole de d epart ou axiome, un ensemble P de productions, qui sont des r` egles de la forme S S1 S2 . . . Sk avec S VN et Si VN VT Compte tenu de lusage que nous en faisons dans le cadre de l ecriture de compilateurs, nous pouvons expliquer ces el ements de la mani` ere suivante : 1. Les symboles terminaux sont les symboles el ementaires qui constituent les cha nes du langage, les phrases. Ce sont donc les unit es lexicales, extraites du texte source par lanalyseur lexical (il faut se rappeler que lanalyseur syntaxique ne conna t pas les caract` eres dont le texte source est fait, il ne voit ce dernier que comme une suite dunit es lexicales). 2. Les symboles non terminaux sont des variables syntaxiques d esignant des ensembles de cha nes de symboles terminaux. epart est un symbole non terminal particulier qui d esigne le langage en son entier. 3. Le symbole de d 4. Les productions peuvent etre interpr et ees de deux mani` eres : comme des r` egles d ecriture (on dit plut ot de r e ecriture ), permettant dengendrer toutes les cha nes correctes. De ce point de vue, la production S S1 S2 . . . Sk se lit pour produire un S correct [sousentendu : de toutes les mani` eres possibles] il fait produire un S1 [de toutes les mani` eres possibles] suivi dun S2 [de toutes les mani` eres possibles] suivi dun . . . suivi dun Sk [de toutes les mani` eres possibles] , egles danalyse, on dit aussi reconnaissance. La production S S1 S2 . . . Sk se lit alors comme des r` pour reconna tre un S , dans une suite de terminaux donn ee, il faut reconna tre un S1 suivi dun S2 suivi dun . . . suivi dun Sk  La d enition dune grammaire devrait donc commencer par l enum eration des ensembles VT et VN . En pratique on se limite ` a donner la liste des productions, avec une convention typographique pour distinguer les symboles terminaux des symboles non terminaux, et on convient que : VT est lensemble de tous les symboles terminaux apparaissant dans les productions, VN est lensemble de tous les symboles non terminaux apparaissant dans les productions, le symbole de d epart est le membre gauche de la premi` ere production. En outre, on all` ege les notations en d ecidant que si plusieurs productions ont le m eme membre gauche
23 J.

Backus a invent e le langage FORTRAN en 1955, P. Naur le langage Algol en 1963.

19

S S1,1 S1,2 . . . S1,k1 S S2,1 S2,2 . . . S2,k2 ... S Sn,1 Sn,2 . . . Sn,kn alors on peut les noter simplement S S1,1 S1,2 . . . S1,k1 | S2,1 S2,2 . . . S2,k2 | . . . | Sn,1 Sn,2 . . . Sn,kn Dans les exemples de ce document, la convention typographique sera la suivante : une cha ne en italiques repr esente un symbole non terminal, une cha ne en caract` eres t el etype ou "entre guillemets", repr esente un symbole terminal. A titre dexemple, voici la grammaire G1 d enissant le langage dont les cha nes sont les expressions arithm etiques form ees avec des nombres, des identicateurs et les deux op erateurs + et *, comme 60 * vitesse + 200 . Suivant notre convention, les symboles non terminaux sont expression, terme et facteur ; le symbole de d epart est expression : expression expression "+" terme | terme terme terme "*" facteur | facteur facteur nombre | identificateur | "(" expression ")" 3.1.2 D erivations et arbres de d erivation (G1 )

rivation. Le processus par lequel une grammaire d De enit un langage sappelle d erivation. Il peut etre formalis e de la mani` ere suivante : Soit G = (VT , VN , S0 , P ) une grammaire non contextuelle, A VN un symbole non terminal et (VT VN ) une suite de symboles, tels quil existe dans P une production A . Quelles que soient les suites de symboles et , on dit que A se d erive en une etape en la suite ce qui s ecrit A Cette d enition justie la d enomination grammaire non contextuelle (on dit aussi grammaire ind ependante du contexte ou context free ). En eet, dans la suite A les cha nes et sont le contexte du symbole A. Ce que cette d enition dit, cest que le symbole A se r e ecrit dans la cha ne quel que soit le contexte , dans le lequel A appara t. Si 0 1 n on dit que 0 se d erive en n en n etapes, et on ecrit 0 n . Enn, si se d erive en en un nombre quelconque, eventuellement nul, d etapes on dit simplement que se d erive en et on ecrit . Soit G = {VT , VN , S0 , P } une grammaire non contextuelle ; le langage engendr e par G est lensemble des cha nes de symboles terminaux qui d erivent de S0 : L (G) = w VT | S0 w Si w L(G) on dit que w est une phrase de G. Plus g en eralement, si (VT VN ) est tel que S0 alors on dit que est une proto-phrase de G. Une proto-phrase dont tous les symboles sont terminaux est une phrase. Par exemple, soit encore la grammaire G1 : expression expression "+" terme | terme terme terme "*" facteur | facteur facteur nombre | identificateur | "(" expression ")" (G1 )
n

et consid erons la cha ne "60 * vitesse + 200" qui, une fois lue par lanalyseur lexical, se pr esente ainsi : w = ( nombre "*" identificateur "+" nombre ). Nous avons expression w, cest ` a dire w L(G1 ) ; en eet, nous pouvons exhiber la suite de d erivations en une etape : 20

expression expression "+" terme terme "+" terme terme "*" facteur "+" terme facteur "*" facteur "+" terme nombre "*" facteur "+" terme nombre "*" identificateur "+" terme nombre "*" identificateur "+" facteur nombre "*" identificateur "+" nombre rivation gauche. La d De erivation pr ec edente est appel ee une d erivation gauche car elle est enti` erement compos ee de d erivations en une etape dans lesquelles ` a chaque fois cest le non-terminal le plus ` a gauche qui est r e ecrit. On peut d enir de m eme une d erivation droite, o` u` a chaque etape cest le non-terminal le plus ` a droite qui est r e ecrit. rivation. Soit w une cha Arbre de de ne de symboles terminaux du langage L(G) ; il existe donc une d erivation telle que S0 w. Cette d erivation peut etre repr esent ee graphiquement par un arbre, appel e arbre de d erivation, d eni de la mani` ere suivante :

expression expression terme terme facteur nombre 60 "*" * identificateur vitesse


Fig. 5 Arbre de d erivation la racine de larbre est le symbole de d epart, les nuds int erieurs sont etiquet es par des symboles non terminaux, si un nud int erieur e est etiquet e par le symbole S et si la production S S1 S2 . . . Sk a et e utilis ee pour d eriver S alors les ls de e sont des nuds etiquet es, de la gauche vers la droite, par S1 , S2 . . . Sk , les feuilles sont etiquet ees par des symboles terminaux et, si on allonge verticalement les branches de larbre (sans les croiser) de telle mani` ere que les feuilles soient toutes ` a la m eme hauteur, alors, lues de la gauche vers la droite, elles constituent la cha ne w. Par exemple, la gure 5 montre larbre de d erivation repr esentant la d erivation donn ee en exemple ci-dessus. On notera que lordre des d erivations (gauche, droite) ne se voit pas sur larbre. 3.1.3 Qualit es des grammaires en vue des analyseurs

terme facteur facteur

"+" +

nombre 200

Etant donn ee une grammaire G = {VT , VN , S0 , P }, faire lanalyse syntaxique dune cha ne w VT cest r epondre ` a la question w appartient-elle au langage L(G) ? . Parlant strictement, un analyseur syntaxique est donc un programme qui nextrait aucune information de la cha ne analys ee, il ne fait quaccepter (par d efaut) ou rejeter (en annon cant une erreurs de syntaxe) cette cha ne. En r ealit e on ne peut pas emp echer les analyseurs den faire un peu plus car, pour prouver que w L(G) il faut exhiber une d erivation S0 w, cest-` a-dire construire un arbre de d erivation dont la liste des feuilles est w. Or, cet arbre de derivation est d ej` a une premi` ere information extraite de la cha ne source, un d ebut de compr ehension  de ce que le texte signie. 21

Nous examinons ici des qualit es quune grammaire doit avoir et des d efauts dont elle doit etre exempte pour que la construction de larbre de d erivation de toute cha ne du langage soit possible et utile. s. Une grammaire est ambigu Grammaires ambigue e sil existe plusieurs d erivations gauches di erentes pour une m eme cha ne de terminaux. Par exemple, la grammaire G2 suivante est ambigu e: expression expression "+" expression | expression "*" expression | facteur facteur nombre | identificateur | "(" expression ")" (G2 )

En eet, la gure 6 montre deux arbres de d erivation distincts pour la cha ne "2 * 3 + 10". Ils correspondent aux deux d erivations gauches distinctes : expression expression "+" expression expression "*" expression "+" expression facteur "*" expression "+" expression nombre "*" expression "+" expression nombre "*" facteur "+" expression nombre "*" nombre "+" expression nombre "*" nombre "+" facteur nombre "*" nombre "+" nombre et expression expression "*" expression facteur "*" expression nombre "*" expression nombre "*" expression "+" expression nombre "*" facteur "+" expression nombre "*" nombre "+" expression nombre "*" nombre "+" facteur nombre "*" nombre "+" nombre

expr expr expr facteur nombre 2 * "*" expr facteur nombre 3 + "+" expr facteur nombre expr facteur nombre

expr "*" expr facteur expr "+" expr facteur

10

nombre nombre * 3 + 10

Fig. 6 Deux arbres de d erivation pour la m eme cha ne Deux grammaires sont dites equivalentes si elles engendrent le m eme langage. Il est souvent possible de remplacer une grammaire ambigu e par une grammaire non ambigu e equivalente, mais il ny a pas une m ethode g en erale pour cela. Par exemple, la grammaire G1 est non ambigu e et equivalente ` a la grammaire G2 ci-dessus. cursives a ` gauche. Une grammaire est r Grammaires re ecursive ` a gauche sil existe un non-terminal A et une d erivation de la forme A A, o` u est une cha ne quelconque. Cas particulier, on dit quon a une r ecursivit e` a gauche simple si la grammaire poss` ede une production de la forme A A. La r ecursivit e` a gauche ne rend pas une grammaire ambigu e, mais emp eche l ecriture danalyseurs pour cette grammaire, du moins des analyseurs descendants24 . Par exemple, la grammaire G1 de la section 3.1.1 est r ecursive ` a gauche, et m eme simplement : expression expression "+" terme | terme terme terme "*" facteur | facteur facteur nombre | identificateur | "(" expression ")" (G1 )

Il existe une m ethode pour obtenir une grammaire non r ecursive ` a gauche equivalente ` a une grammaire donn ee. Dans le cas de la r ecursivit e` a gauche simple, cela consiste ` a remplacer une production telle que
24 La question est un peu pr ematur ee ici, mais nous verrons quun analyseur descendant est un programme compos e de fonctions directement d eduites des productions. Une production A . . . donne lieu ` a une fonction reconnaitre A dont le corps est fait dappels aux fonctions reconnaissant les symboles du membre droit de la production. Dans le cas dune production A A on se retrouve donc avec une fonction reconnaitre A qui commence par un appel de reconnaitre A. Bonjour la r ecursion innie...

22

A A | par les deux productions25 A A A A | En appliquant ce proc ed e` a la grammaire G1 on obtient la grammaire G3 suivante : expression terme n expression n expression "+" terme n expression | terme facteur n terme n terme "*" facteur n terme | facteur nombre | identificateur | "(" expression ")"

(G3 )

A propos des -productions. La transformation de grammaire montr ee ci-dessus a introduit des productions avec un membre droit vide, ou -productions. Si on ne prend pas de disposition particuli` ere, on aura un probl` eme pour l ecriture dun analyseur, puisquune production telle que n expression "+" terme n expression | impliquera notamment que une mani` ere de reconna tre une n expression consiste ` a ne rien reconna tre , ce qui est possible quelle que soit la cha ne dentr ee ; ainsi, notre grammaire semble devenir ambigu e. On r esout ce probl` eme en imposant aux analyseurs que nous ecrirons la r` egle de comportement suivante : dans la d erivation dun non-terminal, une -production ne peut etre choisie que lorsquaucune autre production nest applicable. Dans lexemple pr ec edent, cela donne : si la cha ne dentr ee commence par + alors on doit n ecessairement choisir la premi` ere production. ` gauche. Nous cherchons ` Factorisation a a ecrire des analyseurs pr edictifs. Cela veut dire qu` a tout moment le choix entre productions qui ont le m eme membre gauche doit pouvoir se faire, sans risque derreur, en comparant le symbole courant de la cha ne ` a analyser avec les symboles susceptibles de commencer les d erivations des membres droits des productions en comp etition. Une grammaire contenant des productions comme A 1 | 2 viole ce principe car lorsquil faut choisir entre les productions A 1 et A 2 le symbole courant est un de ceux qui peuvent commencer une d erivation de , et on ne peut pas choisir ` a coup s ur entre 1 et 2 . Une transformation simple, appel ee factorisation ` a gauche, corrige ce d efaut (si les symboles susceptibles de commencer une r e ecriture de 1 sont distincts de ceux pouvant commencer une r e ecriture de 2 ) : A A A 1 | 2 Exemple classique. Les grammaires de la plupart des langages de programmation d enissent ainsi linstruction conditionnelle : instr si si expr alors instr | si expr alors instr sinon instr Pour avoir un analyseur pr edictif il faudra op erer une factorisation ` a gauche : instr si si expr alors instr n instr si n instr si sinon instr | Comme pr ec edemment, lapparition dune -production semble rendre ambigu e la grammaire. Plus pr ecis ement, la question suivante se pose : ny a-t-il pas deux arbres de d erivation possibles pour la cha ne26 : si alors si alors sinon Nous lavons d ej` a dit, on l` eve cette ambigu t e en imposant que la -production ne peut etre choisie que si aucune autre production nest applicable. Autrement dit, si, au moment o` u lanalyseur doit d eriver le nonne dentr ee commence par le terminal sinon, alors la production n instr si terminal n instr si, la cha sinon instr  doit etre appliqu ee. La gure 7 montre larbre de d erivation obtenu pour la cha ne pr ec edente.
25 Pour se convaincre de l equivalence de ces deux grammaires il sut de sapercevoir que, si et sont des symboles terminaux, alors elles engendrent toutes deux le langage {, , , , . . .} 26 Autre formulation de la m eme question : sinon  se rattache-t-il ` a la premi` ere ou ` a la deuxi` eme instruction si  ?

23

instr_si si expr si alors expr alors instr instr fin_instr_si fin_instr_si sinon instr
Fig. 7 Arbre de d erivation pour la cha ne si alors si alors sinon

3.1.4

Ce que les grammaires non contextuelles ne savent pas faire

Les grammaires non contextuelles sont un outil puissant, en tout cas plus puissant que les expressions r eguli` eres, mais il existe des langages (pratiquement tous les langages de programmation, excusez du peu... !) quelles ne peuvent pas d ecrire compl` etement. On d emontre par exemple que le langage L = wcw | w (a|b) , o` u a, b et c sont des terminaux, ne peut pas etre d ecrit par une grammaire non contextuelle. L est fait de phrases comportant deux cha nes de a et b identiques, s epar ees par un c, comme ababcabab. Limportance de cet exemple provient du fait que L mod elise lobligation, quont la plupart des langages, de v erier que les identicateurs apparaissant dans les instructions ont bien et e pr ealablement d eclar es (la premi` ere occurrence de w dans wcw correspond ` a la d eclaration dun identicateur, la deuxi` eme occurrence de w ` a lutilisation de ce dernier). Autrement dit, lanalyse syntaxique ne permet pas de v erier que les identicateurs utilis es dans les programmes font lobjet de d eclarations pr ealables. Ce probl` eme doit n ecessairement etre remis ` a une phase ult erieure danalyse s emantique.

3.2

Analyseurs descendants

Etant donn ee une grammaire G = (VT , VN , S0 , P ), analyser une cha ne de symboles terminaux w VT cest construire un arbre de d erivation prouvant que S0 w. Les grammaires des langages que nous cherchons ` a analyser ont un ensemble de propri et es quon r esume en disant que ce sont des grammaires LL(1). Cela signie quon peut en ecrire des analyseurs : lisant la cha ne source de la gauche vers la droite (gauche = left, cest le premier L), cherchant ` a construire une d erivation gauche (cest le deuxi` eme L), dans lesquels un seul symbole de la cha ne source est accessible ` a chaque instant et permet de choisir, lorsque cest n ecessaire, une production parmi plusieurs candidates (cest le 1 de LL(1)). A propos du symbole accesible. Pour r e echir au fonctionnement de nos analyseurs il est utile dimaginer que la cha ne source est ecrite sur un ruban d elant derri` ere une fen etre, de telle mani` ere quun seul symbole est visible ` a la fois ; voyez la gure 8. Un m ecanisme permet de faire avancer jamais reculer le ruban, pour rendre visible le symbole suivant.

if
chane source dj examine
unit courante

avancer

chane source restant examiner

Fig. 8 Fen etre ` a symboles terminaux

24

Lorsque nous programmerons eectivement des analyseurs, cette machine ` a symboles terminaux  ne sera rien dautre que lanalyseur lexical pr ealablement ecrit ; le symbole visible ` a la fen etre sera repr esent e par une variable uniteCourante, et lop eration faire avancer le ruban  se traduira par uniteCourante = uniteSuivante() (ou bien, si cest lex qui a ecrit lanalyseur lexical, uniteCourante = yylex() ). 3.2.1 Principe

Analyseur descendant. Un analyseur descendant construit larbre de d erivation de la racine (le symbole de d epart de la grammaire) vers les feuilles (la cha ne de terminaux). Pour en d ecrire sch ematiquement le fonctionnement nous nous donnons une fen etre ` a symboles terminaux comme ci-dessus et une pile de symboles, cest-` a-dire une s equence de symboles terminaux et non terminaux a laquelle on ajoute et on enl` ` eve des symboles par une m eme extr emit e, en loccurrence lextr emit e de gauche (cest une pile couch ee ` a lhorizontale, qui se remplit de la droite vers la gauche). Initialisation. Au d epart, la pile contient le symbole de d epart de la grammaire et la fen etre montre le premier symbole terminal de la cha ne dentr ee. ration. Tant que la pile nest pas vide, r Ite ep eter les op erations suivantes si le symbole au sommet de la pile (c.-` a-d. le plus ` a gauche) est un terminal si le terminal visible ` a la fen etre est le m eme symbole , alors d epiler le symbole au sommet de la pile et faire avancer le terminal visible ` a la fen etre, sinon, signaler une erreur (par exemple acher attendu ) ; si le symbole au sommet de la pile est un non terminal S epiler sil y a une seule production S S1 S2 . . . Sk ayant S pour membre gauche alors d S et empiler S1 S2 . . . Sk ` a la place, sil y a plusieurs productions ayant S pour membre gauche, alors dapr` es le terminal visible ` a la fen etre, sans faire avancer ce dernier, choisir lunique production S S1 S2 . . . Sk pouvant convenir, d epiler S et empiler S1 S2 . . . Sk . Terminaison. Lorsque la pile est vide si le terminal visible ` a la fen etre est la marque qui indique la n de la cha ne dentr ee alors lanalyse a r eussi : la cha ne appartient au langage engendr e par la grammaire, sinon, signaler une erreur (par exemple, acher caract` eres inattendus ` a la suite dun texte correct ). A titre dexemple, voici la reconnaissance par un tel analyseur du texte "60 * vitesse + 200" avec la grammaire G3 de la section 3.1.3 : expression terme n expression n expression "+" terme n expression | terme facteur n terme n terme "*" facteur n terme | facteur nombre | identificateur | "(" expression ")"

(G3 )

La cha ne dentr ee est donc (nombre "*" identif "+" nombre). Les etats successifs de la pile et de la fen etre sont les suivants :

25

fen etre nombre nombre nombre nombre "*" "*" identificateur identificateur "+" "+" "+" "+" nombre nombre nombre

pile terme terme terme terme terme terme terme terme n n n n n n n n n n n n n n n expression expression expression expression expression expression expression expression expression expression expression expression expression expression expression expression

facteur n nombre n n "*" facteur n facteur n identificateur n n

"+" terme terme facteur nombre

Lorsque la pile est vide, la fen etre exhibe , la marque de n de cha ne. La cha ne donn ee appartient donc bien au langage consid er e. 3.2.2 Analyseur descendant non r ecursif

Nous ne d evelopperons pas cette voie ici, mais nous pouvons remarquer quon peut r ealiser des programmes it eratifs qui implantent lalgorithme expliqu e` a la section pr ec edente. La plus grosse dicult e est le choix dune production chaque fois quil faut d eriver un non terminal qui est le membre gauche de plusieurs productions de la grammaire. Comme ce choix ne d epend que du terminal visible ` a la fen etre, on peut le faire, et de mani` ere tr` es ecace, ` a laide dune table ` a double entr ee, appel ee table danalyse, calcul ee ` a lavance, repr esentant une fonction Choix : VN VT P qui ` a un couple (S, ) form e dun non terminal (le sommet de la pile) et un terminal (le symbole visible ` a la fen etre) associe la production quil faut utiliser pour d eriver S . Pour avoir un analyseur descendant non r ecursif il sut alors de se donner une fen etre ` a symboles terminaux (cest-` a-dire un analyseur lexical), une pile de symboles comme expliqu e ` a la section pr ec edente, une table danalyse comme expliqu e ici et un petit programme qui implante lalgorithme de la section pr ec edente, dans lequel la partie choisir la production...  se r esume ` a une consultation de la table P = Choix(S, ). En d enitive, un analyseur descendant est donc un couple form e dune table dont les valeurs sont intimement li ees ` a la grammaire analys ee et dun programme tout ` a fait ind ependant de cette grammaire. 3.2.3 Analyse par descente r ecursive

A loppos e du pr ec edent, un analyseur par descente r ecursive est un type danalyseur descendant dans lequel le programme de lanalyseur est etroitement li e` a la grammaire analys ee. Voici les principes de l ecriture dun tel analyseur : eme membre gauche S donne lieu ` a une fonction void recon1. Chaque groupe de productions ayant le m naitre S(void), ou plus simplement void S(void). Le corps de cette fonction se d eduit des membres droits des productions en question, comme expliqu e ci-apr` es. eme membre gauche, le corps de la fonction correspondante est 2. Lorsque plusieurs productions ont le m une conditionnelle (instruction if ) ou un aiguillage (instruction switch ) qui, dapr` es le symbole terminal visible ` a la fen etre, s electionne lex ecution des actions correspondant au membre droit de la production pertinente. Dans cette s election, le symbole visible ` a la fen etre nest pas modi e. 3. Une s equence de symboles S1 S2 . . . Sn dans le membre droit dune production donne lieu, dans la fonction correspondante, ` a une s equence dinstructions traduisant les actions reconnaissance de S1 , reconnaissance de S2 , . . . reconnaissance de Sn . 26

4. Si S est un symbole non terminal, laction reconnaissance de S  se r eduit ` a lappel de fonction reconnaitre S(). 5. Si est un symbole terminal, laction reconnaissance de  consiste ` a consid erer le symbole terminal visible ` a la fen etre et sil est egal ` a , faire passer la fen etre sur le symbole suivant27 , sinon, annoncer une erreur (par exemple, acher attendu ). Lensemble des fonctions ecrites selon les prescriptions pr ec edentes forme lanalyseur du langage consid er e. Linitialisation de lanalyseur consiste ` a positionner la fen etre sur le premier terminal de la cha ne dentr ee. On lance lanalyse en appellant la fonction associ ee au symbole de d epart de la grammaire. Au retour de cette fonction si la fen etre ` a terminaux montre la marque de n de cha ne, lanalyse a r eussi, sinon la cha ne est erron ee28 (on peut par exemple acher le message caract` eres ill egaux apr` es une expression correcte ). Exemple. Voici encore la grammaire G3 de la section 3.1.3 : expression terme n expression n expression "+" terme n expression | terme facteur n terme n terme "*" facteur n terme | facteur nombre | identificateur | "(" expression ")" et voici lanalyseur par descente r ecursive correspondant : void expression(void) { terme(); fin_expression(); } void fin_expression(void) { if (uniteCourante == +) { terminal(+); terme(); fin_expression(); } else /* rien */; } void terme(void) { facteur(); fin_terme(); } void fin_terme(void) { if (uniteCourante == *) { terminal(*); facteur(); fin_terme(); } else /* rien */; } void facteur(void) { if (uniteCourante == NOMBRE) terminal(NOMBRE);
que cest ici le seul endroit, dans cette description, o` u il est indiqu e de faire avancer la fen etre. autre attitude possible -on la vue adopt ee par certains compilateurs de Pascal- consiste ` a ne rien v erier au retour de la fonction associ ee au symbole de d epart. Cela revient ` a consid erer que, si le texte source comporte un programme correct, peu importent les eventuels caract` eres qui pourraient suivre.
28 Une 27 Notez

(G3 )

27

else if (uniteCourante == IDENTIFICATEUR) terminal(IDENTIFICATEUR); else { terminal((); expression(); terminal()); } } La reconnaissance dun terminal revient fr equemment dans un analyseur. Nous en avons fait une fonction s epar ee (on suppose que erreur est une fonction qui ache un message et termine le programme) : void terminal(int uniteVoulue) { if (uniteCourante == uniteVoulue) lireUnite(); else switch (uniteVoulue) { case NOMBRE: erreur("nombre attendu"); case IDENTIFICATEUR: erreur("identificateur attendu"); default: erreur("%c attendu", uniteVoulue); } } Note. Nous avons ecrit le programme pr ec edent en appliquant syst ematiquement les r` egles donn ees plus haut, obtenant ainsi un analyseur correct dont la structure re` ete la grammaire donn ee. Mais il nest pas interdit de pratiquer ensuite certaines simplications, ne serait-ce pour rattraper certaines maladresses de notre d emarche. Lappel g en eralis e de la fonction terminal, notamment, est ` a lorigine de certains test redondants. Par exemple, la fonction n expression commence par les deux lignes if (uniteCourante == +) { terminal(+); ... si on d eveloppe lappel de terminal, la maladresse de la chose devient evidente if (uniteCourante == +) { if (uniteCourante == +) lireUnite(); ... Une version plus raisonnable des fonctions n expression et facteur serait donc : void fin_expression(void) { if (uniteCourante == +) { lireUnite(); terme(); fin_expression(); } else /* rien */; } ... void facteur(void) { if (uniteCourante == NOMBRE) lireUnite(); else if (uniteCourante == IDENTIFICATEUR) lireUnite(); else { 28

terminal((); expression(); terminal()); } } Comme elle est ecrite ci-dessus, la fonction facteur aura tendance ` a faire passer toutes les erreurs par le diagnostic ( attendu , ce qui risque de manquer d` a propos. Une version encore plus raisonnable de cette fonction serait void facteur(void) { if (uniteCourante == NOMBRE) lireUnite(); else if (uniteCourante == IDENTIFICATEUR) lireUnite(); else if (uniteCourante == () { lireUnite((); expression(); terminal()); } else erreur("nombre, identificateur ou ( attendus ici"); }

3.3
3.3.1

Analyseurs ascendants
Principe

Comme nous lavons dit, etant donn ees une grammaire G = {VT , VN , S0 , P } et une cha ne w VT , le but de lanalyse syntaxique est la construction dun arbre de d erivation qui prouve w L(G). Les m ethodes descendantes construisent cet arbre en partant du symbole de d epart de la grammaire et en allant vers  la cha ne de terminaux. Les m ethodes ascendantes, au contraire, partent des terminaux qui constituent la cha ne dentr ee et vont vers  le symbole de d epart. Le principe g en eral des m ethodes ascendantes est de maintenir une pile de symboles29 dans laquelle sont empil es (lempilement sappelle ici d ecalage ) les terminaux au fur et ` a mesure quils sont lus, tant que les symboles au sommet de la pile ne sont pas le membre droit dune production de la grammaire. Si les k symboles du sommet de la pile constituent le membre droit dune production alors ils peuvent etre d epil es et remplac es par le membre gauche de cette production (cette op eration sappelle r eduction ). Lorsque dans la pile il ny a plus que le symbole de d epart de la grammaire, si tous les symboles de la cha ne dentr ee ont et e lus, lanalyse a r eussi. Le probl` eme majeur de ces m ethodes est de faire deux sortes de choix : si les symboles au sommet de la pile constituent le membre droit de deux productions distinctes, laquelle utiliser pour eectuer la r eduction ? lorsque les symboles au sommet de la pile sont le membre droit dune ou plusieurs productions, faut-il r eduire tout de suite, ou bien continuer ` a d ecaler, an de permettre ult erieurement une r eduction plus juste ? A titre dexemple, avec la grammaire G1 de la section 3.1.1 : expression expression "+" terme | terme terme terme "*" facteur | facteur facteur nombre | identificateur | "(" expression ")" (G1 )

voici la reconnaissance par un analyseur ascendant du texte dentr ee "60 * vitesse + 200", cest-` a-dire la cha ne de terminaux (nombre "*" identificateur "+" nombre) :
29 Comme pr ec edemment, cette pile est un tableau couch e` a lhorizontale ; mais cette fois elle grandit de la gauche vers la droite, cest-` a-dire que son sommet est son extr emit e droite.

29

fen etre nombre "*" "*" "*" identificateur "+" "+" "+" "+" nombre

pile nombre facteur terme terme "*" terme "*" terme "*" terme expression expression expression expression expression expression

identificateur facteur

"+" "+" nombre "+" facteur "+" terme

action d ecalage r eduction r eduction d ecalage d ecalage r eduction r eduction r eduction d ecalage d ecalage r eduction r eduction r eduction succ` es

On dit que les m ethodes de ce type eectuent une analyse par d ecalage-r eduction. Comme le montre le tableau ci-dessus, le point important est le choix entre r eduction et d ecalage, chaque fois quune r eduction est possible. Le principe est : les r eductions pratiqu ees r ealisent la construction inverse dune d erivation droite. Par exemple, le r eductions faites dans lanalyse pr ec edente construisent, ` a lenvers, la d erivation droite suivante : expression expression "+" terme expression "+" facteur expression "+" nombre terme "+" nombre terme "*" facteur "+" nombre terme "*" identificateur "+" nombre facteur "*" identificateur "+" nombre nombre "*" identificateur "+" nombre 3.3.2 Analyse LR(k )

Il est possible, malgr e les apparences, de construire des analyseurs ascendants plus ecaces que les analyseurs descendants, et acceptant une classe de langages plus large que la classe des langages trait es par ces derniers. Le principal inconv enient de ces analyseurs est quils n ecessitent des tables quil est extr emement dicile de construire ` a la main. Heureusement, des outils existent pour les construire automatiquement, ` a partir de la grammaire du langage ; la section 3.4 pr esente yacc, un des plus connus de ces outils. Les analyseurs LR(k ) lisent la cha ne dentr ee de la gauche vers la droite (do` u le L), en construisant linverse dune d erivation droite (do` u le R) avec une vue sur la cha ne dentr ee large de k symboles ; lorsquon dit simplement LR on sous-entend k = 1, cest le cas le plus fr equent. Etant donn ee une grammaire G = (VT , VN , S0 , P ), un analyseur LR est constitu e par la donn ee dun ensemble d etats E , dune fen etre ` a symboles terminaux (cest-` a-dire un analyseur lexical), dune pile de doublets (s, e) o` u s E et e VT et de deux tables Action et Suivant, qui repr esentent des fonctions : Action : E VT ({ decaler } E ) ({reduire} P ) { succes, erreur } Suivant : E VN E Un analyseur LR comporte enn un programme, ind ependant du langage analys e, qui ex ecute les op erations suivantes : Initialisation. Placer la fen etre sur le premier symbole de la cha ne dentr ee et vider la pile. It eration. Tant que cest possible, r ep eter : soit s l etat au sommet de la pile et le terminal visible ` a la fen etre si Action(s, ) = (decaler, s ) empiler (, s ) placer la fen etre sur le prochain symbole de la cha ne dentree

30

sinon, si Action(s, ) = (reduire, A ) d epiler autant d el ements de la pile quil y a de symboles dans (soit (, s ) le nouveau sommet de la pile) empiler (A, Suivant(s , A)) sinon, si Action(s, ) = succes arr et sinon erreur. Note. En r ealit e, notre pile est redondante. Les etats de lanalyseur repr esentent les diverses congurations dans lesquelles la pile peut se trouver, il ny a donc pas besoin dempiler les symboles, les etats susent. Nous avons utilis e une pile de couples (etat, symbole) pour clarier lexplication.

3.4

Yacc, un g en erateur danalyseurs syntaxiques

Comme nous lavons indiqu e, les tables Action et Suivant dun analyseur LR sont diciles ` a construire manuellement, mais il existe des outils pour les d eduire automatiquement des productions de la grammaire consid er ee. Le programme yacc 30 est un tel g en erateur danalyseurs syntaxiques. Il prend en entr ee un chier source constitu e essentiellement des productions dune grammaure non contextuelle G et sort ` a titre de r esultat un programme C qui, une fois compil e, est un analyseur syntaxique pour le langage L(G).

grammaire non contextuelle y.tab.c lex.yy.c autres modules source analyser

y.tab.h yacc y.tab.c

gcc

monProg

monProg

rsultat de l'analyse

Fig. 9 Utilisation courante de yacc Dans la description de la grammaire donn ee ` a yacc on peut associer des actions s emantiques aux productions ; ce sont des bouts de code source C que yacc place aux bons endroits de lanalyseur construit. Ce dernier peut ainsi ex ecuter des actions ou produire des informations d eduites du texte source, cest-` a-dire devenir un compilateur. Un analyseur syntaxique requiert pour travailler un analyseur lexical qui lui d elivre le ot dentr ee sous forme dunit es lexicales successives. Par d efaut, yacc suppose que lanalyseur lexical disponible a et e fabriqu e par lex. Autrement dit, sans quil faille de d eclaration sp eciale pour cela, le programme produit par yacc comporte des appels de la fonction yylex aux endroits o` u lacquisition dune unite lexicale est n ecessaire. 3.4.1 Structure dun chier source pour yacc

Un chier source pour yacc doit avoir un nom termin e par .y . Il est fait de trois sections, d elimit ees par deux lignes r eduites au symbole %% : %{ d eclarations pour le compilateur C %}
30 Dans le monde Linux on trouve une version am elior ee de yacc, nomm ee bison, qui appartient ` a la famille GNU. Le nom de yacc provient de yet another compiler compiler ( encore un compilateur de compilateurs... ), bison est issu de la confusion de yacc avec yak ou yack, gros bovin du Tibet.

31

d eclarations pour yacc %% r` egles (productions + actions s emantiques) %% fonctions C suppl ementaires Les parties d eclarations pour le compilateur C  et fonctions C suppl ementaires  sont simplement recopi ees dans le chier produit, respectivement au d ebut et ` a la n de ce chier. Chacune de ces deux parties peut etre absente. Dans la partie d eclarations pour yacc  on rencontre souvent les d eclarations des unit es lexicales, sous une forme qui laisse yacc se charger dinventer des valeurs conventionnelles : %token NOMBRE IDENTIF VARIABLE TABLEAU FONCTION %token OU ET EGAL DIFF INFEG SUPEG %token SI ALORS SINON TANTQUE FAIRE RETOUR Ces d eclarations dunit es lexicales int eressent yacc, qui les utilise, mais aussi lex, qui les manipule en tant que r esultats de la fonction yylex. Pour cette raison, yacc produit31 un chier suppl ementaire, nomm e y.tab.h32 , destin e ` a etre inclus dans le source lex (au lieu du chier que nous avons appel e unitesLexicales.h dans lexemple de la section 2.3.2). Par exemple, le chier produit pour les d eclarations ci-dessus ressemble ` a ceci : #define #define #define ... #define #define NOMBRE IDENTIF VARIABLE FAIRE RETOUR 257 258 259 272 273

Notez que yacc consid` ere que tout caract` ere est susceptible de jouer le r ole dunit e lexicale (comme cela a et e le cas dans notre exemple de la section 2.3.2) ; pour cette raison, ces constantes sont num erot ees ` a partir de 256. cification de VT , VN et S0 . Dans un chier source yacc : Spe les caract` eres simples, encadr es par des apostrophes comme dans les programmes C, et les identicateurs mentionn es dans les d eclarations %token sont tenus pour des symboles terminaux, tous les autres identicateurs apparaissant dans les productions sont consid` eres comme des symboles non terminaux, par d efaut, le symbole de d epart est le membre gauche de la premi` ere r` egle. `gles de traduction. Une r` Re egle de traduction est un ensemble de productions ayant le m eme membre gauche, chacune associ e` a une action s emantique. Une action s emantique (cf. section 3.4.2) est un morceau de code source C encadr e par des accolades. Cest un code que lanalyseur ex ecutera lorsque la production correspondante aura et e utilis ee dans une r eduction. Si on ecrit un analyseur pur , cest-` a-dire un analyseur qui ne fait quaccepter ou rejeter la cha ne dentr ee, alors il ny a pas dactions s emantiques et les r` egles de traduction sont simplement les productions de la grammaire. Dans les r` egles de traduction, le m eta-symbole est indiqu e par deux points :  et chaque r` egle (cesta-dire chaque groupe de productions avec le m ` eme membre gauche) est termin ee par un point-virgule ; . La barre verticale |  a la m eme signication que dans la notation des grammaires. Par exemple, voici comment la grammaire G1 de la section 3.1.1 : expression expression "+" terme | terme terme terme "*" facteur | facteur facteur nombre | identificateur | "(" expression ")" serait ecrite dans un chier source yacc (pour obtenir un analyseur pur, sans actions s emantiques) : %token nombre identificateur %% expression : expression + terme | terme ; terme : terme * facteur | facteur ; facteur : nombre | identificateur | ( expression ) ;
31 Du 32 Dans

(G1 )

moins si on le lance avec loption -d, comme dans yacc -d maSyntaxe.y . le cas de bison les noms des chiers .tab.c  et .tab.h  re` etent le nom du chier source .y .

32

Lanalyseur syntaxique se pr esente comme une fonction int yyparse(void), qui rend 0 lorsque la cha ne dentr ee est accept ee, une valeur non nulle dans le cas contraire. Pour avoir un analyseur syntaxique autonome il sut donc dajouter, en troisi` eme section du chier pr ec edent : %% int main(void) { if (yyparse() == 0) printf("Texte correct\n"); } En r ealit e, il faut aussi ecrire la fonction appel ee en cas derreur. Cest une fonction de prototype void yyerror(char *message), elle est appel ee par lanalyseur avec un message derreur (par d efaut parse error , ce nest pas tr` es informatif !). Par exemple : void yyerror(char *message) { printf(" <<< %s\n", message); } N.B. Leet pr ecis de linstruction ci-dessus, cest-` a-dire la pr esentation eective des messages derreur, d epend de la mani` ere dont lanalyseur lexical ecrit les unit es lexicales au fur et ` a mesure de lanalyse. 3.4.2 Actions s emantiques et valeurs des attributs

Une action s emantique est une s equence dinstructions C ecrite, entre accolades, ` a droite dune production. Cette s equence est recopi ee par yacc dans lanalyseur produit, de telle mani` ere quelle sera ex ecut ee, pendant lanalyse, lorsque la production correspondante aura et e employ ee pour faire une r eduction (cf. analyse par d ecalage-r eduction  ` a la section 3.3). Voici un exemple simple, mais complet. Le programme suivant lit une expression arithm etique inx ee33 form ee de nombres, didenticateurs et des op erateurs + et , et ecrit la repr esentation en notation postx ee34 de la m eme expression. Fichier lexique.l : %{ #include "syntaxe.tab.h" extern char nom[]; /* cha^ ne de caract` eres partag ee avec lanalyseur syntaxique */ %} chiffre [0-9] lettre [A-Za-z] %% [" "\t\n] {chiffre}+ {lettre}({lettre}|{chiffre})* . %% int yywrap(void) { return 1; } Fichier syntaxe.y : %{ char nom[256]; %} /* cha^ ne de caract` eres partag ee avec lanalyseur lexical */ { { { { } yylval = atoi(yytext); return nombre; } strcpy(nom, yytext); return identificateur; } return yytext[0]; }

33 Dans la notation inx ee, un op erateur binaire est ecrit entre ses deux op erandes. Cest la notation habituelle, et elle est ambigu e; cest pourquoi on lui associe un syst` eme dassociativit e et de priorit e des op erateurs, et la possibilit e dutiliser des parenth` eses. 34 Dans la notation postix ee, appel ee aussi notation polonaise inverse, un op erateur binaire est ecrit ` a la suite de ses deux op erandes ; cette notation na besoin ni de priorit es des op erateurs ni de parenth` eses, et elle nest pas ambigu e.

33

%token nombre identificateur %% expression

terme

facteur

: | ; : | ; : | | ;

expression + terme terme terme * facteur facteur nombre identificateur ( expression )

{ printf(" +"); }

{ printf(" *"); }

{ printf(" %d", yylval); } { printf(" %s", nom); }

%% void yyerror(char *s) { printf("<<< \n%s", s); } main() { if (yyparse() == 0) printf(" Expression correcte\n"); } Construction et essai de cet analyseur : $ $ $ $ 2 flex lexique.l bison syntaxe.y gcc lex.yy.c syntaxe.tab.c -o rpn rpn + A * 100 2 A 100 * + Expression correcte $ rpn 2 * A + 100 2 A * 100 + Expression correcte $ rpn 2 * (A + 100) 2 A 100 + * Expression correcte $ Attributs. Un symbole, terminal ou non terminal, peut avoir un attribut, dont la valeur contribue ` a la caract erisation du symbole. Par exemple, dans les langages qui nous int eressent, la reconnaissance du lex` eme "2001" donne lieu ` a lunit e lexicale NOMBRE avec lattribut 2001. Un analyseur lexical produit par lex transmet les attributs des unit es lexicales ` a un analyseur syntaxique produit par yacc ` a travers une variable yylval qui, par d efaut35 , est de type int. Si vous allez voir le chier .tab.h  fabriqu e par yacc et destin e` a etre inclus dans lanalyseur lexical, vous y trouverez, outre les d enitions des codes des unit es lexicales, les d eclarations : #define YYSTYPE int ... extern YYSTYPE yylval; Nous avons dit que les actions s emantiques sont des bouts de code C que yacc se borne ` a recopier dans lanalyseur produit. Ce nest pas tout ` a fait exact, dans les actions s emantiques on peut mettre certains symboles bizarres, que yacc remplace par des expressions C correctes. Ainsi, $1, $2, $3, etc. d esignent les valeurs des attributs des symboles constituant le membre droit de la production concern ee, tandis que $$ d esigne la valeur de lattribut du symbole qui est le membre gauche de cette production.
35 Un m ecanisme puissant et relativement simple permet davoir des attributs polymorphes, pouvant prendre plusieurs types distincts. Nous ne l etudierons pas dans le cadre de ce cours.

34

Laction s emantique { $$ = $1 ; } est implicite et il ny a pas besoin de l ecrire. Par exemple, voici notre analyseur pr ec edent, modi e (l eg` erement) pour en faire un calculateur de bureau eectuant les quatre op erations el ementaires sur des nombres entiers, avec gestion des parenth` eses et de la priorit e des op erateurs : Fichier lexique.l : le m eme que pr ec edemment, ` a ceci pr` es que les identicateurs ne sont plus reconnus. Fichier syntaxe.y : %{ void yyerror(char *); %} %token nombre %% session

expression

terme

facteur

: | ; : | | ; : | | ; : | ;

session expression =

{ printf("r esultat : %d\n", $2); }

expression + terme expression - terme terme terme * facteur terme / facteur facteur nombre ( expression )

{ $$ = $1 + $3; } { $$ = $1 - $3; }

{ $$ = $1 * $3; } { $$ = $1 / $3; }

{ $$ = $2; }

%% void yyerror(char *s) { printf("<<< \n%s", s); } main() { yyparse(); printf("Au revoir!\n"); } Exemple dutilisation ; repr esente la touche n de chier , qui d epend du syst` eme utilis e (Ctrl-D, pour UNIX) : $ go 2 + 3 = r esultat : 5 (2 + 3)*(1002 - 1 - 1) = r esultat : 5000 Au revoir! 3.4.3 Conits et ambigu t es

Voici encore une grammaire equivalente ` a la pr ec edente, mais plus compacte : ... %% session

: session expression = | ;

{ printf("r esultat: %d\n", $2); }

35

expression

: | | | | | ;

expression + expression - expression * expression / ( expression nombre

expression expression expression expression )

{ { { { {

$$ $$ $$ $$ $$

= = = = =

$1 + $3; $1 - $3; $1 * $3; $1 / $3; $2; }

} } } }

%% ... Nous avons vu ` a la section 3.1.3 que cette grammaire est ambigu e ; elle provoquera donc des conits. Lorsquil rencontre un conit36 , yacc applique une r` egle de r esolution par d efaut et continue son travail ; ` a la n de ce dernier, il indique le nombre total de conits rencontr es et arbitrairement r esolus. Il est imp eratif de comprendre la cause de ces conits et il est fortement recommand e dessayer de les supprimer (par exemple en transformant la grammaire). Les conits possibles sont : 1. D ecaler ou r eduire ? ( shift/reduce conict ). Ce conit se produit lorsque lalgorithme de yacc narrive pas ` a choisir entre d ecaler et r eduire, car les deux actions sont possibles et nam` enent pas lanalyse ` a une impasse. Un exemple typique de ce conit a pour origine la grammaire usuelle de linstruction conditionnelle instr si si expr alors instr | si expr alors instr sinon instr Le conit appara t pendant lanalyse dune cha ne comme si alors si alors sinon lorsque le symbole courant est sinon : au sommet de la pile se trouve alors si alors , et la question est : faut-il r eduire ces symboles en instr si (ce qui revient ` a associer la partie sinon au premier si) ou bien faut-il d ecaler (ce qui provoquera plus tard une r eduction revenant ` a associer la partie sinon au second si) ? R esolution par d efaut : lanalyseur fait le d ecalage (cest un comportement glouton  : chacun cherche ` a manger le plus de terminaux possibles). 2. Comment r eduire ? ( reduce/reduce conict ) Ce conit se produit lorsque lalgorithme ne peut pas choisir entre deux productions distinctes dont les membres droits permettent tous deux de r eduire les symboles au sommet de la pile. On trouve un exemple typique dun tel conit dans les grammaires de langages (il y en a !) o` u on note avec des parenth` eses aussi bien les appels de fonctions que les acc` es aux tableaux. Sans rentrer dans les d etails, il est facile dimaginer quon trouvera dans de telles grammaires des productions compl` etement di erentes avec les m emes membres droits. Par exemple, la production d enissant un appel de fonction et celle d enissant un acc` es ` a un tableau pourraient ressembler ` a ceci : ... appel de fonction identicateur ( liste expressions ) ... acces tableau identicateur ( liste expressions ) ... La r esolution par d efaut est : dans lordre o` u les r` egles sont ecrites dans le chier source pour yacc, on pr ef` ere la premi` ere production. Comme lexemple ci-dessus le montre37 , cela ne r esout pas souvent bien le probl` eme. rateurs. La grammaire pr Grammaires dope ec edente, ou du moins sa partie utile, la r` egle expression, v erie ceci : aucune r` egle ne contient d-production, aucune r` egle ne contient une production ayant deux non-terminaux cons ecutifs.
36 Noubliez pas que yacc ne fait pas une analyse, mais un analyseur. Ce quil d etecte en r ealit e nest pas un conit, mais la possibilit e que lanalyseur produit en ait ult erieurement, du moins sur certains textes sources. 37 Le probl` eme consistant ` a choisir assez t ot entre appel de fonction et acc` es ` a un tableau, lorsque les notations sont les m emes, est souvent r esolu par des consid erations s emantiques : lidenticateur qui pr ec` ede la parenth` ese est cens e avoir et e d eclar e, on consulte donc la table de symboles pour savoir si cest un nom de proc edure ou bien un nom de tableau. Du point de vue de la puissance des analyseurs syntaxiques, cest donc plut ot un aveu dimpuissance...

36

De telles grammaires sappellent des grammaires dop erateurs. Nous nen ferons pas l etude d etaill ee ici, mais il se trouve quil est tr` es simple de les rendre non ambigu es : il sut de pr eciser par ailleurs le sens de lassociativit e et la priorit e de chaque op erateur. En yacc, cela se fait par des d eclarations %left et %right qui sp ecient le sens dassociativit e des op erateurs, lordre de ces d eclarations donnant leur priorit e:` a chaque nouvelle d eclaration les op erateurs d eclar es sont plus prioritaires que les pr ec edents. Ainsi, la grammaire pr ec edente, pr esent ee comme ci-apr` es, nest plus ambigu e. On a ajout e des d eclarations indiquant que +, , et / sont associatifs ` a gauche38 et que la priorit e de et / est sup erieure ` a celle de + et . %{ void yyerror(char *); %} %token nombre %left + - %left * / %% session

expression

: | ; : | | | | | ;

session expression =

{ printf("r esultat: %d\n", $2); }

expression + expression - expression * expression / ( expression nombre

expression expression expression expression )

{ { { { {

$$ $$ $$ $$ $$

= = = = =

$1 + $3; $1 - $3; $1 * $3; $1 / $3; $2; }

} } } }

%% ...

Analyse s emantique

Apr` es lanalyse lexicale et lanalyse syntaxique, l etape suivante dans la conception dun compilateur est lanalyse s emantique dont la partie la plus visible est le contr ole de type. Des exemples de t aches li ees au contr ole de type sont : construire et m emoriser des repr esentations des types d enis par lutilisateur, lorsque le langage le permet, traiter les d eclarations de variables et fonctions et m emoriser les types qui leur sont appliqu es, v erier que toute variable r ef erenc ee et toute fonction appel ee ont bien et e pr ealablement d eclar ees, v erier que les param` etres des fonctions ont les types requis, contr oler les types des op erandes des op erations arithm etiques et en d eduire le type du r esultat, au besoin, ins erer dans les expressions les conversion impos ees par certaines r` egles de compatibilit e, etc. Pour xer les id ees, voici une situation typique o` u le contr ole de type joue. Imaginons quun programme, ecrit en C, contient linstruction i = (200 + j) * 3.14 . Lanalyseur syntaxique construit un arbre abstrait repr esentant cette expression, comme ceci (pour etre tout ` a fait corrects, ` a la place de i et j nous aurions d u repr esenter des renvois ` a la table des symboles) :
38 Dire quun op erateur est associatif ` a gauche [resp. ` a droite] cest dire que a b c se lit (a b) c [resp. a (b c)]. La question est importante, m eme pour des op erateurs simples : on tient ` a ce que 100 10 1 vaille 89 et non 91 !

37

= H HH i HH + 3.14 H H 200 j Dans de tels arbres, seules les feuilles (ici i, 200, j et 3.14) ont des types pr ecis39 , tandis que les nuds internes repr esentent des op erations abstraites, dont le type exact reste ` a pr eciser. Le travail s emantique ` a faire consiste ` a remonter les types , depuis les feuilles vers la racine, rendant concrets les op erateurs et donnant un type pr ecis aux sous-arbres. Supposons par exemple que i et j aient et e d eclar ees de type entier. Lanalyse s emantique de larbre pr ec edent permet den d eduire, successivement : que le + est laddition des entiers, puisque les deux op erandes sont entiers, et donc que le sous-arbre chapeaut e par le + repr esente une valeur de type entier, que le est la multiplication des ottants, puisquun op erande est ottant40 , quil y a lieu de convertir lautre op erande vers le type ottant, et que le sous-arbre chapeaut e par repr esente un objet de type ottant, que laectation qui coie larbre tout entier consiste donc en laectation dun ottant ` a un entier, et quil faut donc ins erer une op eration de troncation ottant entier ; en C, il en d ecoule que larbre tout entier repr esente une valeur du type entier. En d enitive, le contr ole de type transforme larbre pr ec edent en quelque chose qui ressemble ` a ceci : aectation dentiers HH H H i ottant entier multiplication ottante HH H entier ottant 3.14 addition enti` ere H H 200 j

4.1

Repr esentation et reconnaissance des types

Une partie importante du travail s emantique quun compilateur fait sur un programme est pendant la compilation des d eclarations, construire des repr esentations des types d eclar es dans le programme, pendant la compilation des instructions, reconna tre les types des objets intervenant dans les expressions. La principale dicult e de ce travail est la complexit e des structures ` a construire et ` a manipuler. En eet, dans les langages modernes les types sont d enis par des proc ed es r ecursifs quon peut composer ` a volont e. Par exemple, en C on peut avoir des entiers, des adresses (ou pointeurs) dentiers, des fonctions rendant des adresses dentiers, des adresses de fonctions rendant des adresses dentiers, des tableaux dadresses de fonctions rendant des adresses dentiers, etc. Cela peut aller aussi loin quon veut, lauteur dun compilateur doit se donner le moyen de repr esenter ces structures de complexit e quelconque. Faute de temps, nous n etudierons pas cet aspect des compilateurs dans le cadre de ce cours.
39 Le type dune constante est donn e par une convention lexicale (exemple : 200 repr esente un entier, 3.14 un ottant), le type dune variable ou le type rendu par une fonction est sp eci e par la d eclaration de la variable ou la fonction en question. 40 Cela sappelle la r` egle du plus fort , elle est suivie par la plupart des langages : lorsque les op erandes dune op eration arithm etique ne sont pas de m eme type, celui dont le type est le plus fort  (plus complexe, plus pr ecis, plus etendu, etc.) tire ` a lui lautre op erande, provoquant une conversion de type.

38

Ainsi, le compilateur que nous r ealiserons ` a titre de projet ne traitera que les types entier et tableau dentiers. Pour les curieux, voici n eanmoins une suggestion de structures de donn ees pour la repr esentation des principaux types du langage C dans un compilateur qui serait lui-m eme41 ecrit en C : typedef enum { tChar, tShort, tInt, tLong, tFloat, tDouble, tPointeur, tTableau, tFonction, tStructure } typePossible; typedef struct listeDescripteursTypes { struct descripteurType *info; struct listeDescripteursTypes *suivant; } listeDescripteursTypes; typedef struct descripteurType { typePossible classe; union { struct { struct descripteurType *typePointe; } casPointeur; struct { int nombreElements; struct descripteurType *typeElement; } casTableau; struct { listeDescripteursTypes *typesChamps; } casStructure; struct { listeDescripteursTypes *typesArguments; struct descripteurType *typeResultat; } casFonction; } attributs; } descripteurType; Ainsi, un type se trouve repr esent e par une structure ` a deux champs : classe, dont la valeur est un code conventionnel qui indique de quelle sorte de type il sagit, et attributs, qui donne les informations n ecessaires pour achever de d enir le type. Attributs est un champ polymorphe (en C, une union ) dont la structure d epend de la valeur du champ classe : si la classe est celle dun type primitif, le champ attributs est sans objet,
une question troublante qui nit par appara tre dans tous les cours de compilation : peut-on ecrire le compilateur dun langage en utilisant le langage quil sagit de compiler ? Malgr e lapparent paradoxe, la chose est tout ` a fait possible. Il faut comprendre que la question nest pas de savoir, par exemple, dans quel langage fut ecrit le tout premier compilateur de C, si tant est quil y eut un jour un compilateur de C alors que la veille il ny en avait pas cela est le probl` eme, peu int eressant, de luf et de la poule. La question est plut ot de savoir si, de nos jours, on peut utiliser un compilateur (existant) de C pour r ealiser un compilateur (nouveau) de C. Pr esent ee comme cela, la chose est parfaitement raisonnable. L` a o` u elle redevient troublante : ce nouveau compilateur de C une fois ecrit, on devra le valider. Pour cela, quel meilleur test que de lui donner ` a compiler... son propre texte source, puisquil est lui-m eme ecrit en C ? Le r esultat obtenu devra etre un nouvel ex ecutable identique ` a celui du compilateur. On voit l` a un des int er ets quil y a ` a ecrire le compilateur dun langage dans le langage ` a compiler : on dispose ipso facto dun formidable test de validation : au terme dun certain processus de construction (on dit plut ot bootstrap ) on doit poss eder un compilateur C capable de compiler son propre texte source S et de donner un r esultat C (S ) v eriant C (S ) = C .
41 Voici

39

si le champ classe indique quil sagit dun type pointeur, alors le champ attributs pointe sur la description du type des objets point es, enir le type : nombreElements, si la valeur du champ classe est tTableau alors il faut deux attributs pour d le nombre de cases du tableau, et typeElement, qui pointe sur la description du type commun des el ements du tableau, sil sagit dun type structure, lattribut est ladresse dune liste cha n ee dont chaque maillon contient un pointeur42 sur un descripteur de type, enn, si le champ classe indique quil sagit dun type fonction, alors le champ attribut se compose de ladresse dun descripteur de type (le type rendu par la fonction) et ladresse dune liste de types (les types des arguments de cette derni` ere). On facilite lallocation dynamique de telles structures en se donnant une fonction : descripteurType *nouveau(typePossible classe) { descripteurType *res = malloc(sizeof(descripteurType)); assert(res != NULL); res->classe = classe; return res; } Pour montrer le fonctionnement de cette structure de donn ees, voici un exemple purement d emonstratif, la construction ` a la main  du descripteur correspondant au type de la variable d eclar ee, en C, comme suit : struct { char *lexeme; int uniteLexicale; } motRes[N]; La variable est motRes (nous lavons utilis ee ` a la section 2.2.2), elle est d eclar ee comme un tableau de N el ements qui sont des structures ` a deux champs : un pointeur sur un caract` ere et un entier. Voici le code qui construit un tel descripteur (point e, ` a la n de la construction, par la variable tmp2) : ... listeDescripteursTypes *pCour, *pSuiv; descripteurType *pTmp1, *pTmp2; /* maillon de liste d ecrivant le type entier */ pCour = malloc(sizeof(listeDescripteursTypes)); pCour->info = nouveau(tInt); pCour->suiv = NULL; /* maillon de liste d ecrivant le type pointeur sur caract` ere */ pSuiv = pCour; pCour = malloc(sizeof(listeDescripteursTypes)); pCour->info = nouveau(tPointeur); pCour->info->attributs.casPointeur.typePointe = nouveau(tChar); pCour->suiv = pSuiv; /* pTmp1 va pointer la description de la structure */ pTmp1 = nouveau(tStructure); pTmp1->attributs.casStructure.typesChamps = pCour; /* pTmp2 va pointer la description du type tableau */ pTmp2 = nouveau(tTableau); pTmp2->attributs.casTableau.nombreElements = N; pTmp2->attributs.casTableau.typeElement = pTmp1; ...
42 Il aurait aussi et e viable de faire que chaque maillon de la liste cha n ee contienne un descripteur de type, au lieu dun pointeur sur un tel descripteur. Apparemment plus lourde ` a g erer, la solution adopt ee ici se r ev` ele ` a lusage la plus souple.

40

Dans le m eme ordre did ees, voici la construction manuelle du descripteur du type matrice de N L N C ottants  ou, plus pr ecis ement, tableau de N L el ements qui sont des tableaux de N C ottants  (en C, cela s ecrit : float matrice[NL][NC] ;). A la n de la construction le descripteur cherch e est point e par pTmp2 : ... /* description dune ligne, tableau de NC flottants: */ pTmp1 = nouveau(tTableau); pTmp1->attributs.casTableau.nombreElements = NC; pTmp1->attributs.casTableau.typeElement = nouveau(tFloat); /* description dune matrice, tableau de NL lignes: */ pTmp2 = nouveau(tTableau); pTmp2->attributs.casTableau.nombreElements = NL; pTmp2->attributs.casTableau.typeElement = pTmp1; ... Enn, pour donner encore un exemple de manipulation de ces structures de donn ees, voici un utilitaire fondamental dans les syst` emes de contr ole de type : la fonction bool eenne qui fournit la r eponse ` a la question deux descripteurs donn es d ecrivent-ils des types identiques ?  : int memeType(descripteurType *a, descripteurType *b) { if (a->classe != b->classe) return 0; switch (a->classe) { case tPointeur: return memeType(a->attributs.casPointeur.typePointe, b->attributs.casPointeur.typePointe); case tTableau: return a->attributs.casTableau.nombreElements == b->attributs.casTableau.nombreElements && memeType(a->attributs.casTableau.typeElement, b->attributs.casTableau.typeElement); case tStructure: return memeListeTypes(a->attributs.casStructure.typesChamps, b->attributs.casStructure.typesChamps); case tFonction: return memeType(a->attributs.casFonction.typeResultat, b->attributs.casFonction.typeResultat) && memeListeTypes(a->attributs.casFonction.typesArguments, b->attributs.casFonction.typesArguments); default: return 1; } } int memeListeTypes(listeDescripteursTypes *a, listeDescripteursTypes *b) { while (a != NULL && b != NULL) { if ( ! memeType(a->info, b->info)) return 0; a = a->suiv; b = b->suiv; } return a == NULL && b == NULL; }

41

4.2

Dictionnaires (tables de symboles)

Dans les langages de programmation modernes, les variables et les fonctions doivent etre d eclar ees avant d etre utilis ees dans les instructions. Quel que soit le degr e de complexit e des types support es par notre compilateur, celui-ci devra g erer une table de symboles, appel ee aussi dictionnaire, dans laquelle se trouveront les identicateurs couramment d eclar es, chacun associ e ` a certains attributs, comme son type, son adresse 43 et dautres informations, cf. gure 10.

identif

type
tEntier

adresse complemt
180

v i t e s s e \0

Fig. 10 Une entr ee dans le dictionnaire Nous etudions pour commencer le cahier des charges du dictionnaire, cest-` a-dire les services que le compilateur en attend, puis, dans les sections suivantes, diverses impl ementations possibles. Grosso modo le dictionnaire fonctionne ainsi : quand le compilateur trouve un identicateur dans une d eclaration, il le cherche dans le dictionnaire en esp erant ne pas le trouver (sinon cest lerreur identicateur d ej` a d eclar e ), puis il lajoute au dictionnaire avec le type que la d eclaration sp ecie, quand le compilateur trouve un identicateur dans la partie ex ecutable44 dun programme, il le cherche dans le dictionnaire avec lespoir de le trouver (sinon cest lerreur identicateur non d eclar e ), ensuite il utilise les informations que le dictionnaire associe ` a lidenticateur. Nous allons voir que la question est un peu plus compliqu ee que cela. 4.2.1 Dictionnaire global & dictionnaire local

Dans les langages qui nous int eressent, un programme est essentiellement une collection de fonctions, entre lesquelles se trouvent des d eclarations de variables. A lint erieur des fonctions se trouvent egalement des d eclarations de variables. Les variables d eclar ees entre les fonctions et les fonctions elles-m emes sont des objets globaux. Un objet global est visible45 depuis sa d eclaration jusqu` a la n du texte source, sauf aux endroits o` u un objet local de m eme nom le masque, voir ci-apr` es. Les variables d eclar ees ` a lint erieur des fonctions sont des objets locaux. Un objet local est visible dans la fonction o` u il est d eclar e, depuis sa d eclaration jusqu` a la n de cette fonction ; il nest pas visible depuis les autres fonctions. En tout point o` u il est visible, un objet local masque 46 tout eventuel objet global qui aurait le m eme nom. En d enitive, quand le compilateur se trouve dans47 une fonction il faut poss eder deux dictionnaires : un dictionnaire global, contenant les noms des objets globaux couramment d eclar es, et un dictionnaire local dans lequel se trouvent les noms des objets locaux couramment d eclar es (qui, parfois, masquent des objets dont les noms se trouvant dans le dictionnaire global). Dans ces conditions, lutilisation des dictionnaires que fait le compilateur se pr ecise :
question des adresses des objets qui se trouvent dans les programmes sera etudi ee en d etail ` a la section 5.1.1. les langages comme C, Java, etc., la partie ex ecutable des programmes est lensemble des corps des fonctions dont le programme se compose. En Pascal il faut ajouter ` a cela le corps du programme. 45 On dit quun objet o ayant le nom n est visible en un point dun programme si une occurrence de n en ce point est comprise comme d esignant o. Cela ne pr ejuge en rien de la correction ou de la l egalit e de lemploi de n en ce point. 46 Par masquage dun objet o par un objet o de m eme nom n on veut dire que o nest pas alt er e ni d etruit, mais devient inaccessible car, dans la portion de programme o` u le masquage a lieu, n d esigne o , non o. 47 Le compilateur lit le programme ` a compiler s equentiellement, du d ebut vers la n. A tout instant il en est ` a un certain endroit du texte source, correspondant ` a la position de lunit e lexicale courante ; quand la compilation progresse, lunit e lexicale avance. Tout cela justie un langage imag e, que nous allons employer, avec des expressions comme le compilateur entre dans la partie ex ecutable  ou le compilateur entre (ou sort ) dune fonction , etc.
44 Dans 43 La

42

Lorsque le compilateur traite la d eclaration dun identicateur i qui se trouve ` a lint erieur dune fonction, i est recherch e dans le dictionnaire local exclusivement ; normalement, il ne sy trouve pas (sinon, erreur : identicateur d ej` a d eclar e ). Suite ` a cette d eclaration, i est ajout e au dictionnaire local. Il ny a strictement aucun int er et ` a savoir si i gure ` a ce moment-l` a dans le dictionnaire global. Lorsque le compilateur traite la d eclaration dun identicateur i en dehors de toute fonction, i est recherch e dans le dictionnaire global, qui est le seul dictionnaire existant en ce point ; normalement, il ne sy trouve pas (sinon, erreur : identicateur d ej` a d eclar e ). Suite ` a cette d eclaration, i est ajout e au dictionnaire global. Lorsque le compilateur compile une instruction ex ecutable, forc ement ` a lint erieur dune fonction, chaque identicateur i rencontr e est recherch e dabord dans le dictionnaire local ; sil ne sy trouve pas, il est recherch e ensuite dans le dictionnaire global (si les deux recherches echouent, erreur : identicateur non d eclar e ). En proc edant ainsi on assure le masquage des objets globaux par les objets locaux. Lorsque le compilateur quitte une fonction, le dictionnaire local en cours dutilisation est d etruit, puisque les objets locaux ne sont pas visibles ` a lext erieur de la fonction. Un dictionnaire local nouveau, vide, est cr e e lorsque le compilateur entre dans une fonction. Notez ceci : tout au long dune compilation le dictionnaire global ne diminue jamais. A lint erieur dune fonction il naugmente pas ; le dictionnaire global naugmente que lorsque le dictionnaire local nexiste pas. 4.2.2 Tableau ` a acc` es s equentiel

Limpl ementation la plus simple des dictionnaires consiste en un tableau dans lequel les identicateurs sont plac es dans lordre o` u leurs d eclarations ont et e trouv ees dans le texte source. Dans ce tableau, les recherches sont s equentielles. Voyez la gure 11 : lorsquil existe, le dictionnaire local se trouve au-dessus du dictionnaire global (en supposant que le tableau grandit du bas vers le haut).
maxDico
xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx

maxDico

sommet

base

dictionnaire local dictionnaire global

sommet

xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxx

dictionnaire global
base (b)

0 (a)

Fig. 11 Dictionnaires, quand on est ` a lint erieur (a) et ` a lext erieur (b) des fonctions Trois variables sont essentielles dans la gestion du dictionnaire : maxDico est le nombre maximum dentr ees possibles (` a ce propos, voir Augmentation de la taille du dictionnaire  un peu plus loin), ees valides dans le dictionnaire ; on doit avoir sommet maxDico, sommet est le nombre dentr el ement du dictionnaire du dessus (cest-` a-dire le dictionnaire local quand il y en a deux, base est le premier le dictionnaire global quand il ny en a quun). Avec tout cela, la manipulation du dictionnaire devient tr` es simple. Les op erations n ecessaires sont : 1. Recherche dun identicateur pendant le traitement dune d eclaration (que ce soit ` a lint erieur dune fonction ou ` a lext erieur de toute fonction) : rechercher lidenticateur dans la portion de tableau comprise entre les bornes sommet 1 et base, ecutable : rechercher lidentica2. Recherche dun identicateur pendant le traitement dune expression ex teur en parcourant dans le sens des indices d ecroissants 48 la portion de tableau comprise entre les bornes sommet 1 et 0,
48 En

parcourant le tableau du haut vers le bas on assure le masquage dun objet global par un objet local de m eme nom.

43

3. Ajout dune entr ee dans le dictionnaire (que ce soit ` a lint erieur dune fonction ou ` a lext erieur de toute fonction) : apr` es avoir v eri e que sommet < maxDico, placer la nouvelle entr ee ` a la position sommet, puis faire sommet sommet + 1, 4. Creation dun dictionnaire local, au moment de lentr ee dans une fonction : faire base sommet, 5. Destruction du dictionnaire local, ` a la sortie dune fonction : faire sommet base puis base 0. Augmentation de la taille du dictionnaire. Une question technique assez aga cante quil faut r egler lors de limpl ementation dun dictionnaire par un tableau est le choix de la taille ` a donner ` a ce tableau, etant entendu quon ne conna t pas ` a lavance la grosseur (en nombre de d eclarations) des programmes que notre compilateur devra traiter. La biblioth` eque C ore un moyen pratique pour r esoudre ce probl` eme, la fonction realloc qui permet daugmenter la taille dun espace allou e dynamiquement tout en pr eservant le contenu de cet espace. Voici, ` a titre dexemple, la d eclaration et les fonctions de gestion dun dictionnaire r ealis e dans un tableau ayant au d epart la place pour 50 el ements ; chaque fois que la place manque, le tableau est agrandi dautant quil faut pour loger 25 nouveaux el ements : #include <stdlib.h> #include <stdio.h> #include <string.h> typedef struct { char *identif; int type; int adresse; int complement; } ENTREE_DICO; #define TAILLE_INITIALE_DICO #define INCREMENT_TAILLE_DICO ENTREE_DICO *dico; int maxDico, sommet, base; void creerDico(void) { maxDico = TAILLE_INITIALE_DICO; dico = malloc(maxDico * sizeof(ENTREE_DICO)); if (dico == NULL) erreurFatale("Erreur interne (pas assez de m emoire)"); sommet = base = 0; } void agrandirDico(void) { maxDico = maxDico + INCREMENT_TAILLE_DICO; dico = realloc(dico, maxDico); if (dico == NULL) erreurFatale("Erreur interne (pas assez de m emoire)"); } void erreurFatale(char *message) { fprintf(stderr, "%s\n", message); exit(-1); } Pour montrer une utilisation de tout cela, voici la fonction qui ajoute une entr ee au dictionnaire : void ajouterEntree(char *identif, int type, int adresse, int complement) { if (sommet >= maxDico) 44 50 25

agrandirDico(); dico[sommet].identif = malloc(strlen(identif) + 1); if (dico[sommet].identif == NULL) erreurFatale("Erreur interne (pas assez de m emoire)"); strcpy(dico[sommet].identif, identif); dico[sommet].type = type; dico[sommet].adresse = adresse; dico[sommet].complement = complement; sommet++; } 4.2.3 Tableau tri e et recherche dichotomique

Limpl ementation des dictionnaires expliqu ee ` a la section pr ec edente est facile ` a mettre en uvre et susante pour des applications simples, mais pas tr` es ecace (la complexit e des recherches est, en moyenne, de lordre de n ementations plus 2 , soit O (n) ; les insertions se font en temps constant). Dans la pratique on recherche des impl ecaces, car un compilateur passe beaucoup de temps49 ` a rechercher des identicateurs dans les dictionnaires. Une premi` ere am elioration des dictionnaires consiste ` a maintenir des tableaux ordonn es, permettant des recherches par dichotomie (la complexit e dune recherche devient ainsi O(log2 n), cest beaucoup mieux). La gure 11 est toujours valable, mais maintenant il faut imaginer que les el ements dindices allant de base ` a sommet 1 et, lorsquil y a lieu, ceux dindices allant de 0 ` a base 1, sont plac es en ordre croissant des identicateurs. Dans un tel contexte, voici la fonction existe, qui eectue la recherche de lidenticateur repr esent e par ident dans la partie du tableau, suppos e ordonn e, comprise entre les indices inf et sup, bornes incluses. Le r esultat de la fonction (1 ou 0, interpr et es comme vrai ou faux ) est la r eponse ` a la question l el ement cherch e se trouve-t-il dans le tableau ? . En outre, au retour de la fonction, la variable point ee par ptrPosition contient la position de l el ement recherch e, cest-` a-dire : si lidenticateur est dans le tableau, lindice de lentr ee correspondante, si lidenticateur ne se trouve pas dans le tableau, lindice auquel il faudrait ins erer, les cas ech eant, une entr ee concernant cet identicateur. int existe(char *identif, int inf, int sup, int *ptrPosition) { int i, j, k; i = inf; j = sup; while (i <= j) { /* invariant: i <= position <= j + 1 */ k = (i + j) / 2; if (strcmp(dico[k].identif, identif) < 0) i = k + 1; else j = k - 1; } /* ici, de plus, i > j, soit i = j + 1 */ *ptrPosition = i; return i <= sup && strcmp(dico[i].identif, identif) == 0; } Voici la fonction qui ajoute une entr ee au dictionnaire : void ajouterEntree(int position, char *identif, int type, int adresse, int complt) { int i; if (sommet >= maxDico) agrandirDico();
49 On

estime que plus de 50% du temps dune compilation est d epens e en recherches dans les dictionnaires.

45

for (i = sommet - 1; i >= position; i--) dico[i + 1] = dico[i]; sommet++; dico[position].identif = malloc(strlen(identif) + 1); if (dico[position].identif == NULL) erreurFatale("Erreur interne (pas assez de m emoire)"); strcpy(dico[position].identif, identif); dico[position].type = type; dico[position].adresse = adresse; dico[position].complement = complt; } La fonction ajouterEntree utilise un param` etre position dont la valeur provient de la fonction existe. Pour xer les id ees, voici la fonction qui traite la d eclaration dun objet local : ... int placement; ... if (existe(lexeme, base, sommet - 1, &placement)) erreurFatale("identificateur d ej` a d eclar e"); else { ici se place lobtention des informations type, adresse, complement, etc. ajouterEntree(placement, lexeme, type, adresse, complement); } ... Nous constatons que lutilisation de tableaux tri es permet doptimiser la recherche, dont la complexit e passe de O(n) ` a O(log2 n), mais p enalise les insertions, dont la complexit e devient O(n), puisqu` a chaque insertion il faut pousser dun cran la moiti e (en moyenne) des el ements du dictionnaire. Or, pendant la compilation dun programme il y a beaucoup dinsertions et on ne peut pas n egliger a priori le poids des insertions dans le calcul du co ut de la gestion des identicateurs. Il y a cependant une t ache, qui nest pas la gestion du dictionnaire mais lui est proche, o` u on peut sans r eserve employer un tableau ordonn e, cest la gestion dune table de mots r eserv es, comme celle de la section 2.2.2. En eet, le compilateur, ou plus pr ecis ement lanalyseur lexical, fait de nombreuses recherches dans cette table qui ne subit jamais la moindre insertion. 4.2.4 Arbre binaire de recherche

Cette section suppose la connaissance de la structure de donn ees arbre binaire. Les arbres binaires de recherche ont les avantages des tables ordonn ees, pour ce qui est de lecacit e de la recherche, sans leurs inconv enients puisque, etant des structures cha n ees, linsertion dun el ement noblige pas a pousser ceux qui dun point de vue logique se placent apr` ` es lui. Un arbre binaire de recherche, ou ABR, est un arbre binaire etiquet e par des valeurs appartenant ` a un ensemble ordonn e, v eriant la propri et e suivante : pour tout nud p de larbre pour tout nud q appartenant au sous-arbre gauche de p on a q inf o pinf o, pour tout nud r appartenant au sous-arbre droit de p on a rinf o pinf o. Voici, par exemple, lABR obtenu avec les identicateurs  Denis, Fernand, Bernard, Andr e, Gaston, Ernest et Charles, ajout es ` a larbre successivement et dans cet ordre50 : Denis H HH

Bernard H H H Andr e Charles

HH Fernand H HH Ernest Gaston

50 Le nombre d el ements et, surtout, lordre dinsertion font que cet ABR est parfaitement equilibr e. En pratique, les choses ne se passent pas aussi bien.

46

Techniquement, un tel arbre serait r ealis e dans un programme comme le montre la gure 12.
Denis autres infos

Bernard autres infos autres infos

Fernand

...

...

...

Fig. 12 R ealisation eective des maillons dun ABR Pour r ealiser les dictionnaires par des ABR il faudra donc se donner les d eclarations : typedef struct noeud { ENTREE_DICO info; struct noeud *gauche, *droite; } NOEUD; NOEUD *dicoGlobal = NULL, *dicoLocal = NULL; Voici la fonction qui recherche le nud correspondant ` a un identicateur donn e. Elle rend ladresse du nud cherch e, ou NULL en cas d echec de la recherche : NOEUD *rechercher(char *identif, NOEUD *ptr) { while (ptr != NULL) { int c = strcmp(identif, ptr->info.identif); if (c == 0) return ptr; else if (c < 0) ptr = ptr->gauche; else ptr = ptr->droite; } return NULL; } Cette fonction est utilis ee pour rechercher des identicateurs apparaissant dans les parties ex ecutables des fonctions. Elle sera donc appel ee de la mani` ere suivante : ... p = rechercher(lexeme, dicoLocal); if (p == NULL) { p = recherche(lexeme, dicoGlobal); if (p == NULL) erreurFatale("identificateur non d eclar e"); } exploitation des informations du nud point e par p ... Pendant la compilation des d eclarations, les recherches se font avec la fonction suivante, qui eectue la recherche et, dans la foul ee, lajout dun nouveau nud. Dans cette fonction, la rencontre dun nud associ e a lidenticateur quon cherche est consid ` er ee comme une erreur grave. La fonction rend ladresse du nud nouvelle cr e e: 47

...

NOEUD *insertion(NOEUD **adrDico, char *identif, int type, int adresse, int complt) { NOEUD *ptr; if (*adrDico == NULL) return *adrDico = nouveauNoeud(identif, type, adresse, complt); ptr = *adrDico; for (;;) { int c = strcmp(identif, ptr->info.identif); if (c == 0) erreurFatale("identificateur deja d eclar e"); if (c < 0) if (ptr->gauche != NULL) ptr = ptr->gauche; else return ptr->gauche = nouveauNoeud(identif, type, adresse, complt); else if (ptr->droite != NULL) ptr = ptr->droite; else return ptr->droite = nouveauNoeud(identif, type, adresse, complt); } } Exemple dappel (cas des d eclarations locales) : ... p = insertion( &dicoLocal, lexeme, leType, lAdresse, leComplement); ... N.B. Dans la fonction insertion, le pointeur de la racine du dictionnaire dans lequel il faut faire linsertion est pass e par adresse, cest pourquoi il y a deux dans la d eclaration NOEUD **adrDico. Cela ne sert qu` a couvrir le cas de la premi` ere insertion, lorsque le dictionnaire est vide : le pointeur point e par adrDico (en pratique il sagit soit de dicoLocal soit de dicoGlobal ) vaut NULL et doit changer de valeur. Cest beaucoup de travail pour pas grand-chose, on l eviterait en d ecidant que les dictionnaires ne sont jamais vides (il sut de leur cr eer doce un nud bidon  sans signication). . Limpl Restitution de lespace alloue ementation dun dictionnaire par un ABR poss` ede lecacit e de la recherche dichotomique, car ` a chaque comparaison on divise par deux la taille de lensemble susceptible de contenir l el ement cherch e, sans ses inconv enients, puisque le temps n ecessaire pour faire une insertion dans un ABR est n egligeable. H elas, cette m ethode a deux d efauts : la recherche nest dichotomique que si larbre est equilibr e, ce qui ne peut etre suppos e que si les identicateurs sont tr` es nombreux et uniform ement r epartis, la destruction dun dictionnaire est une op eration beaucoup plus complexe que dans les m ethodes qui utilisent un tableau. La destruction dun dictionnaire, en loccurrence le dictionnaire local, doit se faire chaque fois que le compilateur sort dune fonction. Cela peut se programmer de la mani` ere suivante : void liberer(NOEUD *dico) { if (dico != NULL) { liberer(dico->gauche); liberer(dico->droite); free(dico); } } Comme on le voit, pour rendre lespace occup e par un ABR il faut le parcourir enti` erement (alors que dans le cas dun tableau la modication dun index sut). Il y a un moyen de rendre beaucoup plus simple la lib eration de lespace occup e par un arbre. Cela consiste a ` ecrire sa propre fonction dallocation, quon utilise ` a la place malloc, et qui alloue un espace dont on ma trise la remise ` a z ero. Par exemple : 48

#define MAX_ESPACE 1000 NOEUD espace[MAX_ESPACE]; int niveauAllocation = 0; NOEUD *monAlloc(void) { if (niveauAllocation >= MAX_ESPACE) return NULL; else return &espace[niveauAllocation++]; } void toutLiberer(void) { niveauAllocation = 0; } 4.2.5 Adressage dispers e

Une derni` ere technique de gestion dune table de symboles m erite d etre mentionn ee ici, car elle est tr` es utilis ee dans les compilateurs r eels. Cela sappelle adressage dispers e, ou hash-code 51 . Le principe en est assez simple : au lieu de rechercher la position de lidenticateur dans la table, on obtient cette position par un calcul sur les caract` eres de lidenticateur ; si on sen tient ` a des op erations simples, un calcul est certainement plus rapide quune recherche. Soit I lensemble des identicateurs existant dans un programme, N la taille de la table didenticateurs. Lid eal serait de poss eder une fonction h : I [0, N [ qui serait rapide, facile ` a calculer, injective, cest-` a-dire qui ` a deux identicateurs distincts ferait correspondre deux valeurs distinctes. On ne dispose g en eralement pas dune telle fonction car lensemble I des identicateurs pr esents dans le programme nest pas connu a priori. De plus, la taille de la table nest souvent pas susante pour permettre linjectivit e (qui requiert N |I |). On se contente donc dune fonction h prenant, sur lensemble des identicateurs possibles, des valeurs uniform ement r eparties sur lintervalle [0, N [. Cest-` a-dire que h nest pas injective, mais si N |I |, on esp` ere que les couples didenticateurs i1 , i2 tels que i1 = i2 et h(i1 ) = h(i2 ) (on appelle cela une collision ) sont peu nombreux, si N < |I |, les collisions sont in evitables. Dans ce cas on souhaite quelles soient egalement r eparties : pour |I | chaque j [0, N [ le nombre de i I tels que h(i) = j est ` a peu pr` es le m eme, cest-` a-dire N . Il est facile de voir pourquoi : h etant la fonction qui place  les identicateurs dans la table, il sagit d eviter que ces derniers samoncellent ` a certains endroits de la table, tandis qu` a dautres endroits cette derni` ere est peu remplie, voire pr esente des cases vides. Il est dicile de dire ce quest une bonne fonction de hachage. La litt erature sp ecialis ee rapporte de nombreuses recettes, mais il ny a probablement pas de solution universelle, car une fonction de hachage nest bonne que par rapport ` a un ensemble didenticateurs donn e. Parmi les conseils quon trouve le plus souvent : prenez N premier (une des recettes les plus donn ees, mais plus personne ne se donne la peine den rappeler la justication), utilisez des fonctions qui font intervenir tous les caract` eres des identicateurs ; en eet, dans les programmes on rencontre souvent des grappes  de noms, par exemple : poids, poids1, poidsTotal, poids maxi, etc. ; une fonction qui ne ferait intervenir que les cinq premiers caract` eres ne serait pas tr` es bonne ici, ecrivez des fonctions qui donnent comme r esultat de grandes valeurs ; lorsque ces valeurs sont ramen ees ` a lintervalle [0, N [, par exemple par une op eration de modulo, les eventuels d efauts (dissym etries, accumulations, etc.) de la fonction initiale ont tendance ` a dispara tre. Une fonction assez souvent utilis ee consiste ` a consid erer les caract` eres dun identicateur comme les coecients dun polyn ome P (X ) dont on calcule la valeur pour un X donn e (ou, ce qui revient au m eme, ` a voir un
51 La technique expliqu ee ici est celle dite adressage dispers e ouvert. Il en existe une autre, ladressage dispers e ferm e, dans laquelle toute linformation se trouve dans le tableau adress e par la fonction de hachage (il ny a pas de listes cha n ees associ es aux cases du tableau).

49

identicateur comme l ecriture dun nombre dans une certaine base). En C, cela donne la fonction : int hash(char *ident, int N) { const int X = 23; int r = 0; while (*ident != \0) r = X * r + *(ident++); return r % N; }
fonction de hachage identif Ensemble des identificateurs table de hachage infos suivant

/* why not? */

Fig. 13 Adressage dispers e ouvert L` a o` u les m ethodes dadressage dispers e di erent entre elles cest dans la mani` ere de g erer les collisions. Dans le cas de ladressage dispers e ouvert, la table quon adresse ` a travers la fonction de hachage nest pas une table didenticateurs, mais une table de listes cha n ees dont les maillons portent des identicateurs (voyez la gure 13). Si on note T cette table, alors Tj est le pointeur de t ete de la liste cha n ee dans laquelle sont les identicateurs i tels que h(i) = j . Vu de cette mani` ere, ladressage dispers e ouvert appara t comme une m ethode de partitionnement de lensemble des identicateurs. Chaque liste cha n ee est un compartiment de la partition. Si la fonction h est bien faite, les compartiments ont ` a peu pr` es la m eme taille. Lecacit e de la m ethode provient alors du fait quau lieu de faire une recherche dans une structure de taille |I | on fait un calcul et une recherche dans une structure I| de taille |N . Voici la fonction de recherche : typedef struct maillon { char *identif; autres informations struct maillon *suivant; } MAILLON; #define N 101 MAILLON *table[N]; MAILLON *recherche(char *identif) { MAILLON *p; for (p = table[hash(identif, N)]; p != NULL; p = p->suivant) if (strcmp(identif, p->identif) == 0) return p; return NULL; } et voici la fonction qui se charge de linsertion dun identicateur (suppos e absent) : MAILLON *insertion(char *identif) { int h = hash(identif, N); 50

return table[h] = nouveauMaillon(identif, table[h]); } avec une fonction nouveauMaillon d enie comme ceci : MAILLON *nouveauMaillon(char *identif, MAILLON *suivant) { MAILLON *r = malloc(sizeof(MAILLON)); if (r == NULL) erreurFatale("Erreur interne (probl` eme despace)"); r->identif = malloc(strlen(identif) + 1); if (r->identif == NULL) erreurFatale("Erreur interne (probl` eme despace)"); strcpy(r->identif, identif); r->suivant = suivant; return r; }

Production de code

Nous nous int eressons ici ` a la derni` ere phase de notre compilateur, la production de code. Dans ce but, nous pr esentons certains aspects de larchitecture des machines (registres et m emoire, adresses, pile dex ecution, compilation s epar ee et edition de liens, etc.) et en m eme temps nous introduisons une machine virtuelle, la machine Mach 1, pour laquelle notre compilateur produit du code et dont nous ecrirons un simulateur. Faute de temps, nous faisons limpasse sur certains aspects importants (et diciles) de la g en eration de code, et notamment sur les algorithmes pour loptimisation du code et pour lallocation des registres.

5.1
5.1.1

Les objets et leurs adresses


Classes dobjets

Dans les langages qui nous int eressent les programmes manipulent trois classes dobjets : 1. Les objets statiques existent pendant toute la dur ee de lex ecution dun programme ; on peut consid erer que lespace quils occupent est allou e par le compilateur pendant la compilation52 . Les objets statiques sont les fonctions, les constantes et les variables globales. Ils sont garnis de valeurs initiales : pour une fonction, son code, pour une constante, sa valeur et pour une variable globale, une valeur initiale explicit ee par lauteur du programme (dans les langages qui le permettent) ou bien une valeur initiale implicite, souvent z ero. Les objets statiques peuvent etre en lecture seule ou en lecture- ecriture 53 . Les fonctions et les constantes sont des objets statiques en lecture seule. Les variables globales sont des objets statiques en lecture- ecriture. On appelle espace statique lespace m emoire dans lequel sont log es les objets statiques. Il est g en eralement constitu e de deux zones : la zone du code, o` u sont les fonctions et les constantes, et lespace global, o` u sont les variables globales. Ladresse dun objet statique est un nombre entier qui indique la premi` ere (parfois lunique) cellule de la m emoire occup ee par lobjet. Elle est presque toujours exprim ee comme un d ecalage54 par rapport au d ebut de la zone contenant lobjet en question. 2. Les objets automatiques sont les variables locales des fonctions, ce qui comprend :
52 En r ealit e, le compilateur nalloue pas la m emoire pendant la compilation ; au lieu de cela, il en produit une certaine repr esentation dans le chier objet, et cest un outil appel e chargeur qui, apr` es traitement du chier objet par l editeur de lien, installe les objets statiques dans la m emoire. Mais, pour ce qui nous occupe ici, cela revient au m eme. 53 Lorsque le syst` eme le permet, les objets en lecture seule sont log es dans des zones de la m emoire dans lesquelles les tentatives d ecriture, cest-` a-dire de modication de valeurs, sont d etect ees et signal ees ; les petits syst` emes norent pas cette s ecurit e (qui limite les d eg ats lors des utilisations inad equates des pointeurs et des indices des tableaux). 54 Le mot d ecalage (les anglophones disent oset ) fait r ef erence ` a une m ethode dadressage employ ee tr` es souvent : une entit e est rep er ee par un couple (base,d ecalage ), o` u base est une adresse connue et d ecalage un nombre entier quil faut ajouter ` a base pour obtenir ladresse voulue. Lacc` es t[i] au i -` eme el ement dun tableau t en est un exemple typique, dans lequel t (ladresse de t[0]) est la base et i le d ecalage.

51

les variables d eclar ees ` a lint erieur des fonctions, les arguments formels de ces derni` eres. Ces variables occupent un espace qui nexiste pas pendant toute la dur ee de lex ecution du programme, mais uniquement lorsquil est utile. Plus pr ecis ement, lactivation dune fonction commence par lallocation dun espace, appel e espace local de la fonction, de taille susante pour contenir ses arguments et ses variables locales (plus un petit nombre dinformations techniques additionnelles expliqu ees en d etail ` a la section 5.2.4). Un espace local nouveau est allou e chaque fois quune fonction est appel ee, m eme si cette fonction etait d ej` a active et donc quun espace local pour elle existait d ej` a (cest le cas dune fonction qui sappelle elle-m eme, directement ou indirectement). Lorsque lactivation dune fonction se termine son espace local est d etruit.

sens d'empilement espace local de h espace local de f espace local de g espace local de f

Fig. 14 Empilement despaces locaux (f a appel e g qui a appel e f qui a appel e h) La propri et e suivante est importante : chaque fois quune fonction se termine, la fonction qui se termine est la plus r ecemment activ ee de celles qui ont et e commenc ees et ne sont pas encore termin ees. Il en d ecoule que les espaces locaux des fonctions peuvent etre allou es dans une m emoire g er ee comme une pile (voyez la gure 14) : lorsquune fonction est activ ee, son espace local est cr e e au-dessus des espaces locaux des fonctions actives (i.e. commenc ees et non termin ees) ` a ce moment-l` a. Lorsquune fonction se termine, son espace local est celui qui se trouve au sommet de la pile et il sut de le d epiler pour avoir au sommet lespace local de la fonction qui va reprendre le contr ole. 3. Les objets dynamiques sont allou es lorsque le programme le demande explicitement (par exemple ` a travers la fonction malloc de C ou lop erateur new de Java et C++). Si leur destruction nest pas explicitement demand ee (fonction free de C, op erateur delete de C++) ces objets existent jusqu` a la terminaison du programme55 . Les objets dynamiques ne peuvent pas etre log es dans les zones o` u sont les objets statiques, puisque leur existence nest pas certaine, elle d epend des conditions dex ecution. Ils ne peuvent pas davantage etre h eberg es dans la pile des espaces locaux, puisque cette pile grandit et diminue en accord avec les appels et retours des fonctions, alors que les moments de la cr eation et de la destruction des objets dynamiques ne sont pas connus a priori. On place donc les objets dynamiques dans un troisi` eme espace, distinct des deux pr ec edents, appel e le tas (heap, en anglais). La gestion du tas, cest-` a-dire son allocation sous forme de morceaux de tailles variables, ` a la demande du programme, la r ecup eration des morceaux rendus par ce dernier, la lutte contre l emiettement de lespace disponible, etc. font lobjet dalgorithmes savants impl ement es dans des fonctions comme malloc et free ou des op erateurs comme new et delete. En d enitive, la m emoire utilis ee par un programme en cours dex ecution est divis ee en quatre espaces (voyez la gure 15) :
55 Dans certains langages, comme Java, cest un m ecanisme de r ecup eration de la m emoire inutilis ee qui se charge de d etruire les objets dynamiques dont on peut prouver quils ninterviendront plus dans le programme en cours.

52

tas (objets dynamiques)

espace disponible

pile (objets locaux) espace global code

espace statique

TEG TC

Fig. 15 Organisation de la m emoire

le code (espace statique en lecture seule), contenant le programme et les constantes, lespace global (espace statique en lecture- ecriture), contenant les variables globales, la pile (stack ), contenant variables locales, le tas (heap ), contenant les variables allou ees dynamiquement.

Les tailles du code et de lespace global sont connues d` es la n de la compilation et ne changent pas pendant lex ecution du programme. La pile et le tas, en revanche, evoluent au cours de lex ecution : le tas ne fait que grossir56 , la pile grossit lors des appels de fonctions et diminue lors des terminaisons des fonctions appel ees. La rencontre de la pile et du tas (voyez la gure 15) est un accident mortel pour lex ecution du programme. Cela se produit lorsquun programme alloue trop dobjets dynamiques et/ou de taille trop importante, ou bien lorsquun programme fait trop dappels de fonctions et/ou ces derni` eres ont des espaces locaux de taille trop importante. 5.1.2 Do` u viennent les adresses des objets ?

Puisque ladresse dun objet est un d ecalage par rapport ` a la base dun certain espace qui d epend de la classe de lobjet, du point de vue du compilateur obtenir ladresse dun objet  cest simplement m emoriser l etat courant dun certain compteur dont la valeur exprime le niveau de remplissage de lespace correspondant. Cela suppose que dautres op erations du compilateur font par ailleurs grandir ce compteur, de sorte que si un peu plus tard on cherche ` a obtenir ladresse dun autre objet on obtiendra une valeur di erente. Plus pr ecis ement, pendant quil produit la traduction dun programme, le compilateur utilise les trois variables suivantes : es (des TC (Taille du Code) Pendant la compilation, cette variable a constamment pour valeur le nombre dunit octets, des mots, cela d epend de la machine) du programme en langage machine couramment produites. Au d ebut de la compilation, TC vaut 0. Ensuite, si memoire repr esente lespace (ou le chier) dans lequel le code machine est m emoris e, la production dun el ement de code, comme un opcode ou un op erande, se traduit par les deux aectations : memoire[T C ] element TC TC + 1
56 En eet, le r ole des fonctions de restitution de m emoire (free, delete ) est de bien g erer les parcelles de m emoire emprunt ee puis rendue par un programme ; ces fonctions limitent la croissance du tas, mais elles ne sont pas cens ees en r eduire la taille.

53

Par cons equent, pour m emoriser ladresse dune fonction fon il sut de faire, au moment o` u commence la compilation de la fonction : adresse(f on) T C TEG (Taille de lEspace Global) Cette variable a constamment pour valeur la somme des tailles des variables globales dont le compilateur a rencontr e la d eclaration. Au d ebut de la compilation TEG vaut 0. Par la suite, le r ole de TEG dans la d eclaration dune variable globale varg se r esume ` a ceci : adresse(varg ) T EG T EG T EG + taille(varg ) o` u taille(varg ) repr esente la taille de la variable en question, qui d epend de son type. erieur dune fonction, cette variable a pour valeur la somme des tailles TEL (Taille de lEspace Local) A lint des variables locales de la fonction en cours de compilation. Si le compilateur nest pas dans une fonction TEL nest pas d eni. A lentr ee de chaque fonction TEL est remis ` a z ero. Par la suite, le r ole de TEL dans la d eclaration dune variable locale varl se r esume ` a ceci : adresse(varl) T EL T EL T EL + taille(varl) Les arguments formels de la fonction, bien qu etant des objets locaux, ninterviennent pas dans le calcul de TEL (la question des adresses des arguments formels sera trait ee ` a la section 5.2.4). A la n de la compilation les valeurs des variables TC et TEG sont pr ecieuses : TC est la taille du code donc, dans une organisation comme celle de la gure 15, elle est aussi le d ecalage (relatif ` a lorigine g en erale de la m emoire du programme) correspondant ` a la base de lespace global, TEG est la taille de lespace global ; par cons equent, dans une organisation comme celle de la gure 15, T C + T EG est le d ecalage (relatif ` a lorigine g en erale de la m emoire du programme) correspondant au niveau initial de la pile. 5.1.3 Compilation s epar ee et edition de liens

Tout identicateur apparaissant dans une partie ex ecutable dun programme doit avoir et e pr ealablement d eclar e. La d eclaration dun identicateur i, que ce soit le nom dune variable locale, dune variable globale ou dune fonction, produit son introduction dans le dictionnaire ad equat, associ e` a une adresse, notons-la adri . Par la suite, le compilateur remplace chaque occurrence de i dans une expression par le nombre adri . On peut donc penser que dans le code qui sort dun compilateur les identicateurs qui se trouvaient dans le texte source ont disparu57 et, de fait, tel peut etre le cas dans les langages qui obligent ` a mettre tout le programme dans un seul chier. Mais les choses sont plus compliqu ees dans les langages, comme C ou Java, o` u le texte dun programme peut se trouver eclat e dans plusieurs chiers sources destin es ` a etre compil es ind ependamment les uns des autres (on appelle cela la compilation s epar ee ). En eet, dans ces langages il doit etre possible quune variable ou une fonction d eclar ee dans un chier soit mentionn ee dans un autre. Cela implique qu` a la n de la compilation il y a dans le chier produit quelque trace des noms des variables et fonctions mentionn ees dans le chier source. Notez que cette question ne concerne que les objets globaux. Les objets locaux, qui ne sont d ej` a pas visibles en dehors de la fonction dans laquelle ils sont d eclar es, ne risquent pas d etre visibles dans un autre chier. Notez egalement que le principal int eress e par cette aaire nest pas le compilateur, mais un outil qui lui est associ e, l editeur de liens (ou linker ) dont le r ole est de concat ener plusieurs chiers objets, r esultats de compilations s epar ees, pour en faire un unique programme ex ecutable, en v eriant que les objets r ef erenc es mais non d enis dans certains chiers sont bien d enis dans dautres chiers, et en compl etant de telles r ef erences insatisfaites  par les adresses des objets correspondants. Faute de temps, le langage dont nous ecrirons le compilateur ne supportera pas la compilation s epar ee. Nous naurons donc pas besoin d editeur de liens dans notre syst` eme.
57 Une cons equence tangible de la disparition des identicateurs est limpossibilit e de d ecompiler  les chiers objets, ce qui nest pas grave, mais aussi la dicult e de les d eboguer, ce qui est plus emb etant. Cest pourquoi la plupart des compilateurs ont une option de compilation qui provoque la conservation des identicateurs avec le code, an de permettre aux d ebogueurs dacc eder aux variables et fonctions par leurs noms originaux.

54

Pour les curieux, voici n eanmoins quelques explications sur l editeur de liens et la structure des chiers objets dans les langages qui autorisent la compilation s epar ee. Appelons module un chier produit par le compilateur. Le r ole de l editeur de liens est de concat ener (mettre bout ` a bout) plusieurs modules. En g en eral, il concat` ene dun c ot e les zones de code (objets statiques en lecture seule) de chaque module, et dun autre cot e les espaces globaux (objets statiques en lecture ecriture) de chaque module, an dobtenir une unique zone de code totale et un unique espace global total. Un probl` eme appara t imm ediatement : si on ne fait rien, les adresses des objets statiques, qui sont exprim ees comme des d eplacements relatifs ` a une base propre ` a chaque module, vont devenir fausses, sauf pour le module qui se retrouve en t ete. Cest facile ` a voir : chaque module apporte une fonction dadresse 0 et probablement une variable globale dadresse 0. Dans lex ecutable nal, une seule fonction et une seule variable globale peuvent avoir ladresse 0. Il faut donc que l editeur de liens passe en revue tout le contenu des modules quil concat` ene et en corrige toutes les r ef erences ` a des objets statiques, pour tenir compte du fait que le d ebut de chaque module ne correspond plus ` a ladresse 0 mais ` a une adresse egale ` a la somme des tailles des modules qui ont et e mis devant lui. fe rences absolues et re fe rences relatives. En pratique le travail mentionn Re e ci-dessus se trouve all eg e par le fait que les langages-machines supportent deux mani` eres de faire r ef erence aux objets statiques. On dit quune instruction fait une r ef erence absolue ` a un objet statique (variable ou fonction) si ladresse de ce dernier est indiqu ee par un d ecalage relatif ` a la base de lespace concern e (lespace global ou lespace du code). Nous venons de voir que ces r ef erences doivent etre corrig ees lorsque le module qui les contient est d ecal e et ne commence pas lui-m eme, dans le chier qui sort de l editeur de liens, ` a ladresse 0. On dit quune instruction fait une r ef erence relative ` a un objet statique lorsque ce dernier est rep er e par un d ecalage relatif ` a ladresse ` a laquelle se trouve la r ef erence elle-m eme.
base

fonction A

fonction A

y
APPELA x APPELR y

Fig. 16 R ef erence absolue et r ef erence relative ` a une m eme fonction A Par exemple, la gure 16 montre le cas dun langage machine dans lequel il y a deux instructions pour appeler une fonction : APPELA, qui prend comme op erande une r ef erence absolue, et APPELR qui prend une r ef erence relative. Lint er et des r ef erences relatives saute aux yeux : elles sont insensibles aux d ecalages du module dans lequel elles se trouvent. Lorsque l editeur de liens concat` ene des modules pour former un ex ecutable, les r ef erences relatives contenues dans ces modules nont pas ` a etre mises ` a jour. Bien entendu, cela ne concerne que lacc` es aux objets qui se trouvent dans le m eme module que la r ef erence ; il ny a aucun int er et ` a repr esenter par une r ef erence relative un acc` es ` a un objet dun autre module. Ainsi, une r` egle suivie par les compilateurs, lorsque le langage machine le permet, est : produire des r ef erences intra-modules relatives et des r ef erences inter-modules absolues. Structure des modules. Il nous reste ` a comprendre comment l editeur de liens arrive ` a attribuer ` a une r ef erence ` a un objet non d eni dans un module (cela sappelle une r ef erence insatisfaite ) ladresse de lobjet, qui se trouve dans un autre module.

55

Pour cela il faut savoir que chaque chier produit par le compilateur se compose de trois parties : une section de code et deux tables faites de couples (identicateur, adresse dans la section de code ) : la section de code contient la traduction en langage machine dun chier source ; ce code comporte un certain nombre de valeurs incorrectes, ` a savoir les r ef erences ` a des objets externes (i.e. non d enis dans ce module) dont ladresse nest pas connue au moment de la compilation, la table des r ef erences insatisfaites de la section de code ; dans cette table, chaque identicateur r ef erenc e mais non d eni est associ e ` a ladresse de l el ement de code, au contenu incorrect, quil faudra corriger lorsque ladresse de lobjet sera connue, la table des objets publics 58 d enis dans le module ; dans cette table, chaque identicateur est le nom dun objet que le module en question met ` a la disposition des autres modules, et il est associ e` a ladresse de lobjet concern e dans la section de code .
section de code rfrences insatisfaites ... fonction g ...

x y
fonction f

... APPELA ...

???

objets publics ... fonction f ...

Fig. 17 Un module objet Par exemple, la gure 17 repr esente un module contenant la d enition dune fonction publique f et lappel dune fonction non d enie dans ce module g. En r esum e, le travail de l editeur de liens se compose sch ematiquement des t aches suivantes : concat enation des sections de code des modules donn es ` a lier ensemble, d ecalage des r ef erences absolues contenues dans ces modules (dont les m emoris ees dans les tables), r eunion des tables dobjets publics et utilisation de la table obtenue pour corriger les r ef erences insatisfaites pr esentes dans les sections de code, emission du chier ex ecutable nal, form e du code corrig e.

5.2

La machine Mach 1

Nous continuons notre expos e sur la g en eration de code par la pr esentation de la machine Mach 159 , la machine cible du compilateur que nous r ealiserons ` a titre de projet. 5.2.1 Machines ` a registres et machines ` a pile

Les langages evolu es permettent l ecriture dexpressions en utilisant la notation alg ebrique ` a laquelle nous sommes habitu es, comme X = Y + Z , cette formule signiant ajoutez le contenu de Y ` a celui de Z et rangez le r esultat dans X  (X , Y et Z correspondent ` a des emplacements dans la m emoire de lordinateur). Une telle expression est trop compliqu ee pour le processeur, il faut la d ecomposer en des instructions plus simples. La nature de ces instructions plus simples d epend du type de machine dont on dispose. Relativement a la mani` ` ere dont les op erations sont exprim ees, il y a deux grandes familles de machines :
58 Quels sont les objets publics, cest-` a-dire les objets d enis dans un module quon peut r ef erencer depuis dautres modules ? On la d ej` a dit, un objet public est n ecessairement global. Inversement, dans certains langages, tout objet global est public. Dans dautres langages, aucun objet nest public par d efaut et il faut une d eclaration explicite pour rendre publics les objets globaux que le programmeur souhaite. Enn, dans des langages comme le C, tous les objets globaux sont publics par d efaut, et une qualication sp eciale (dans le cas de C cest la qualication static) permet den rendre certains priv es, cest-` a-dire non publics. 59 Cela se prononce, au choix, m` eque ouane  ou machun .

56

1. Les machines ` a registres poss` edent un certain nombre de registres, not es ici R1, R2, etc., qui sont les seuls composants susceptibles dintervenir dans une op erations (autre quun transfert de m emoire ` a registre ou r eciproquement) ` a titre dop erandes ou de r esultats. Inversement, nimporte quel registre peut intervenir dans une op eration arithm etique ou autre ; par cons equent, les instructions qui expriment ces op erations doivent sp ecier leurs op erandes. Si on vise une telle machine, laectation X = Y + Z devra etre traduite en quelque chose comme (notant X, Y et Z les adresses des variables X , Y et Z ) : MOVE MOVE ADD MOVE Y,R1 Z,R2 R1,R2 R2,X // // // // d eplace la valeur de Y dans R1 d eplace la valeur de Z dans R2 ajoute R1 ` a R2 d eplace la valeur de R2 dans X

2. Les machines ` a pile, au contraire, disposent dune pile (qui peut etre la pile des variables locales) au sommet de laquelle se font toutes les op erations. Plus pr ecis ement : les op erandes dun op erateur binaire sont toujours les deux valeurs au sommet de la pile ; lex ecution dune op eration binaire consiste toujours ` a d epiler deux valeurs x et y et ` a empiler le r esultat x y de lop eration, lop erande dun op erateur unaire est la valeur au sommet de la pile ; lex ecution dune op eration unaire consiste toujours ` a d epiler une valeur x et ` a empiler le r esultat x de lop eration. Pour une telle machine, le code X = Y + Z se traduira donc ainsi (notant encore X, Y et Z les adresses des variables X , Y et Z ) : PUSH PUSH ADD POP Y Z X // // // // met la valeur de Y au sommet de la pile met la valeur de Z au sommet de la pile remplace les deux valeurs au sommet de la pile par leur somme enl` eve la valeur au sommet de la pile et la range dans X

Il est a priori plus facile de produire du code de bonne qualit e pour une machine ` a pile plut ot que pour une machine ` a registres, mais ce d efaut est largement corrig e dans les compilateurs commerciaux par lutilisation de savants algorithmes qui optimisent lutilisation des registres. Car il faut savoir que les machines physiques  existantes sont presque toujours des machines ` a registres, pour une raison decacit e facile ` a comprendre : les op erations qui ne concernent que des registres restent internes au microprocesseur et ne sollicitent ni le bus ni la m emoire de la machine60 . Et, avec un compilateur optimisateur, les expressions complexes sont traduites par des suites dinstructions dont la plupart ne mentionnent que des registres. Les algorithmes doptimisation et dallocation des registres sortant du cadre de ce cours, la machine pour laquelle nous g en ererons du code sera une machine ` a pile. 5.2.2 Structure g en erale de la machine Mach 1

La m emoire de la machine Mach 1 est faite de cellules num erot ees, organis ees comme le montre la gure 18 (cest la structure de la gure 15, le tas en moins). Les registres suivants ont un r ole essentiel dans le fonctionnement de la machine : CO (Compteur Ordinal) indique constamment la cellule contenant linstruction que la machine est en train dex ecuter, BEG (Base de lEspace Global) indique la premi` ere cellule de lespace r eserv e aux variables globales (autrement dit, BEG pointe la variable globale dadresse 0), e lespace local de la fonction BEL (Base de lEspace Local) indique la cellule autour de laquelle est organis en cours dex ecution ; la valeur de BEL change lorsque lactivation dune fonction commence ou nit (` a ce propos voir la section 5.2.4), SP (Sommet de la Pile) indique constamment le sommet de la pile, ou plus exactement la premi` ere cellule libre au-dessus de la pile, cest-` a-dire le nombre total de cellules occup ees dans la m emoire.
60 Se rappeler que de nos jours, d ecembre 2001, le microprocesseur dun ordinateur personnel moyennement puissant tourne ` a2 GHz (cest sa cadence interne) alors que son bus et sa m emoire ne travaillent qu` a 133 MHz.

57

espace disponible SP pile

BEL

espace global BEG CO code

Fig. 18 Organisation de la m emoire de la machine Mach 1

5.2.3

Jeu dinstructions

La machine Mach 1 est une machine ` a pile. Chaque instruction est faite soit dun seul opcode, elle occupe alors une cellule, soit dun opcode et un op erande, elle occupe alors deux cellules cons ecutives. Les constantes enti` eres et les adresses  ont la m eme taille, qui est celle dune cellule. La table 1, ` a la n de ce polycopi e, donne la liste des instructions. 5.2.4 Compl ements sur lappel des fonctions

La cr eation et la destruction de lespace local des fonctions a lieu ` a quatre moments bien d etermin es : lactivation de la fonction, dabord du point de vue de la fonction appelante (1), puis de celui de la fonction appel ee (2), ensuite le retour, dabord du point de vue de la fonction appel ee (3) puis de celui de la fonction appelante (4). Voici ce qui se passe (voyez la gure 19) : 1. La fonction appelante r eserve un mot vide sur la pile, o` u sera d epos e le r esultat de la fonction, puis elle empile les valeurs des arguments eectifs. Ensuite, elle ex ecute une instruction APPEL (qui empile ladresse de retour). 2. La fonction appel ee empile le BEL courant  (qui est en train de devenir BEL pr ec edent ), prend la valeur de SP pour BEL courant, puis alloue lespace local. Pendant la dur ee de lactivation de la fonction : les variables locales sont atteintes ` a travers des d eplacements (positifs ou nuls) relatifs ` a BEL : 0 premi` ere variable locale, 1 deuxi` eme variable locale, etc. les arguments formels sont egalement atteints ` a travers des d eplacements (n egatifs, cette fois) relatifs ` a BEL : 3 n-` eme argument, 4 (n 1)-` eme argument, etc. u la fonction doit d eposer son r esultat est atteinte elle aussi ` a travers un d eplacement relatif la cellule o` a BEL. Ce d ` eplacement est (n + 3), n etant le nombre darguments formels, et suppose donc laccord entre la fonction appelante et la fonction appel ee au sujet du nombre darguments de la fonction appel ee. Lensemble de toutes ces valeurs forme l espace local  de la fonction en cours. Au-del` a de cet espace, la pile est utilis ee pour le calcul des expressions courantes. 3. Terminaison du travail : la fonction appel ee remet BEL et SP comme ils etaient lorsquelle a et e activ ee, puis eectue une instruction RETOUR. ere lespace occup e par les valeurs des 4. Reprise du calcul interrompu par lappel. La fonction appelante lib` arguments eectifs. Elle se retrouve alors avec une pile au sommet de laquelle est le r esultat de la fonction qui vient d etre appel ee. Globalement, la situation est la m eme quapr` es une op eration arithm etique : les op erandes ont et e d epil es et remplac es par le r esultat.

58

haut de la pile

SP valuation en cours var. loc. k ... var. loc. 1 BEL prcdent adr. retour arg n ... arg 1 rsultat espace local de la fonction appelante
bas de la pile

BEL

Fig. 19 Espace local dune fonction

e lespace local dune fonction ? Les arguments dune fonction et les variables d Qui cre eclar ees ` a lint erieur de cette derni` ere sont des objets locaux et ont donc des adresses qui sont des d eplacements relatifs ` a BEL (positifs dans le cas des variables locales, n egatifs dans le cas des arguments, voir la gure 19). Lexplication pr ec edente montre quil y a une di erence importante entre ces deux sortes dobjets locaux : les arguments sont install es dans la pile par la fonction appelante, car ce sont les valeurs dexpressions qui doivent etre evalu ees dans le contexte de la fonction appelante. Il est donc naturel de donner ` a cette derni` ere la responsabilit e de les enlever de la pile, au retour de la fonction appel ee, les variables locales sont allou ees par la fonction appel ee, qui est la seule ` a en conna tre le nombre ; cest donc cette fonction qui doit les enlever de la pile, lorsquelle se termine (linstruction SORTIE fait ce travail). Les conventions dappel. Il y a donc un ensemble de conventions, dites conventions dappel, qui pr ecisent ` quel endroit la fonction appelante doit d a eposer les valeurs des arguments et o` u trouvera-t-elle le r esultat ou, ce qui revient au m eme, ` a quel endroit la fonction appel ee trouvera ses arguments et o` u doit-elle d eposer son r esultat. En g en eral les conventions dappel ne sont pas laiss ees ` a lappr eciation des auteurs de compilateurs. Edict ees par les concepteurs du syst` eme dexploitation, elles sont suivies par tous les compilateurs homologu es , ce qui a une cons equence tr` es importante : puisque tous les compilateurs produisent des codes dans lesquels les param` etres et le r esultat des fonctions sont pass es de la m eme mani` ere, une fonction ecrite dans un langage L1 , et compil ee donc par le compilateur de L1 , pourra appeler une fonction ecrite dans un autre langage L2 et compil ee par un autre compilateur, celui de L2 . Nous avons choisi ici des conventions dappel simples et pratiques (beaucoup de syst` emes font ainsi) puisque : es dans lordre dans lequel ils se pr esentent dans lexpression dappel, les arguments sont empil le r esultat est d epos e l` a o` u il faut pour que, apr` es nettoyage des arguments, il soit au sommet de la pile. Mais il y a un petit inconv enient61 . Dans notre syst` eme, les argument arg1 , arg2 , ... argn sont atteints, ` a lint erieur de la fonction, par les adresses locales respectives (n + 3) + 1, (n + 3) + 2, ... (n + 3) + n = 3, et le r esultat de la fonction lui-m eme poss` ede ladresse locale (n + 3). Cela oblige la fonction appel ee ` a conna tre
61 Et encore, ce nest pas sur que ce soit un inconv enient, tellement il semble naturel dexiger, comme en Pascal ou Java, que la fonction appelante et la fonction appel ee soient daccord sur le nombre darguments de cette derni` ere.

59

le nombre n darguments eectifs, et interdit l ecriture de fonctions admettant un nombre variable darguments (de telles fonctions sont possibles en C, songez ` a printf et scanf).

5.3

Exemples de production de code

Quand on nest pas connaisseur de la question on peut croire que la g en eration de code machine est la partie la plus importante dun compilateur, et donc que cest elle qui d etermine la structure g en erale de ce dernier. On est alors surpris, voire frustr e, en constatant la place consid erable que les ouvrages sp ecialis es consacrent ` a lanalyse (lexicale, syntaxique, s emantique, etc.). Cest que62 : La bonne mani` ere d ecrire un compilateur du langage L pour la machine M consiste ` a ecrire un analyseur du langage L auquel, dans un deuxi` eme temps, on ajoute sans rien lui enlever les op erations qui produisent du code pour la machine M . Dans cette section nous expliquons, ` a travers des exemples, comment ajouter ` a notre analyseur les op erations de g en eration de code qui en font un compilateur. Il faudra se souvenir de ceci : quand on r e echit ` a la g en eration de code on est concern e par deux ex ecutions di erentes : dune part lex ecution du compilateur que nous r ealisons, qui produit comme r esultat un programme P en langage machine, dautre part lex ecution du programme P ; a priori cette ex ecution a lieu ult erieurement, mais nous devons limaginer en m eme temps que nous ecrivons le compilateur, pour comprendre pourquoi nous faisons que ce dernier produise ce quil produit. 5.3.1 Expressions arithm etiques

Puisque la machine Mach 1 est une machine ` a pile, la traduction des expressions arithm etiques, aussi complexes soient-elles, est extr emement simple et el egante. A titre dexemple, imaginons que notre langage source est une sorte de C francis e , et consid erons la situation suivante (on suppose quil ny a pas dautres d eclarations de variables que celles quon voit ici) : entier x, y, z; ... entier uneFonction() { entier a, b, c; ... y = 123 * x + c; } Pour commencer, int eressons-nous ` a lexpression 123 * x + c. Son analyse syntaxique aura et e faite par des fonctions expArith (expression arithm etique) et nExpArith ressemblant ` a ceci : void expArith(void) { terme(); finExpArith(); } void finExpArith(void) { if (uniteCourante == + || uniteCourante == -) { uniteCourante = uniteSuivante(); terme(); finExpArith(); } } (les fonctions terme et nTerme sont analogues aux deux pr ec edentes, avec et / dans les r oles de + et , et facteur dans le r ole de terme ). Enn, une version simpli ee63 de la fonction facteur pourrait commencer comme ceci :
g en eralement : la bonne fa con dobtenir linformation port ee par un texte soumis ` a une syntaxe consiste ` a ecrire lanalyseur syntaxique correspondant et, dans un deuxi` eme temps, ` a lui ajouter les op erations qui construisent les informations en question. Ce principe est tout ` a fait fondamental. Si vous ne deviez retenir quune seule chose de ce cours, que ce soit cela. 63 Attention, nous envisageons momentan ement un langage ultra-simple, sans tableaux ni appels de fonctions, dans lequel une occurrence dun identicateur dans une expression indique toujours ` a une variable simple.
62 Plus

60

void facteur(void) { if (uniteCourante == NOMBRE) uniteCourante = uniteSuivante(); else if (uniteCourante == IDENTIFICATEUR) uniteCourante = uniteSuivante(); else ... } Le principe de fonctionnement dune machine ` a pile, expliqu e` a la section 5.2.1, a la cons equence fondamentale suivante : 1. La compilation dune expression produit une suite dinstructions du langage machine (plus ou moins longue, selon la complexit e de lexpression) dont lex ecution a pour eet global dajouter une valeur au sommet de la pile, ` a savoir le r esultat de l evaluation de lexpression. 2. La compilation dune instruction du langage source produit une suite dinstructions du langage machine (plus ou moins longue, selon la complexit e de lexpression) dont lex ecution laisse la pile dans l etat o` u elle se trouvait avant de commencer linstruction en question. Une mani` ere de retrouver quel doit etre le code produit pour la compilation de lexpression 123 * x + c consiste ` a se dire que 123 est d ej` a une expression correcte ; leet dune telle expression doit etre de mettre au sommet de la pile la valeur 123. Par cons equent, la compilation de lexpression 123 doit donner le code EMPC 123 De m eme, la compilation de lexpression x doit donner EMPG EMPL 0 2 (car x est, dans notre exemple, la premi` ere variable globale) et celle de lexpression c doit donner (car c est la troisi` eme variable locale). Pour obtenir ces codes il sut de transformer la fonction facteur comme ceci : void facteur(void) { if (uniteCourante == NOMBRE) { genCode(EMPC); genCode(atoi(lexeme)); uniteCourante = uniteSuivante(); } else if (uniteCourante == IDENTIFICATEUR) { ENTREE_DICO *pDesc = rechercher(lexeme); genCode(pDesc->classe == VARIABLE_GLOBALE ? EMPG : EMPL); genCode(pDesc->adresse); uniteCourante = uniteSuivante(); } else ... } La fonction genCode se charge de placer un el ement (un opcode ou un op erande) dans le code. Si nous supposons que ce dernier est rang e dans la m emoire, ce pourrait etre tout simplement : void genCode(int element) { mem[TC++] = element; } Pour la production de code, les fonctions expArith et terme nont pas besoin d etre modi ees. Seules les fonctions dans lesquelles des op erateurs apparaissent explicitement doivent l etre :

61

void finExpArith(void) { int uc = uniteCourante; if (uniteCourante == + || uniteCourante == -) { uniteCourante = uniteSuivante(); terme(); genCode(uc == + ? ADD : SOUS); finExpArith(); } } void finterme(void) { int uc = uniteCourante; if (uniteCourante == * || uniteCourante == /) { uniteCourante = uniteSuivante(); facteur(); genCode(uc == * ? MUL : DIV); finTerme(); } } Au nal, le code produit ` a loccasion de la compilation de lexpression 123 * x + c sera donc : EMPC 123 EMPG 0 MUL EMPL 2 ADD et, en imaginant lex ecution de la s equence ci-dessus, on constate que son eet global aura bien et e dajouter une valeur au sommet de la pile. Consid erons maintenant linstruction compl` ete y = 123 * x + c. Dans un compilateur ultra-simpli e qui ignorerait les appels de fonction et les tableaux, on peut imaginer quelle aura et e analys ee par une fonction instruction commen cant comme ceci : void instruction(void) { if (uniteCourante == IDENTIFICATEUR) { uniteCourante = uniteSuivante(); terminal(=); expArith(); terminal(;); } else ... } /* instruction daffectation */

pour quil produise du code il faut transformer cet analyseur comme ceci : void instruction(void) { if (uniteCourante == IDENTIFICATEUR) { /* instruction daffectation */ ENTREE_DICO *pDesc = rechercher(lexeme); uniteCourante = uniteSuivante(); terminal(=); expArith(); genCode(pDesc->classe == VARIABLE_GLOBALE ? DEPG : DEPL); genCode(pDesc->adresse); terminal(;); } else ... } 62

on voit alors que le code nalement produit par la compilation de y = 123 * x + c aura et e: EMPC 123 EMPG 0 MUL EMPL 2 ADD DEPG 1 (y est la deuxi` eme variable globale). Comme pr evu, lex ecution du code pr ec edent laisse la pile comme elle etait en commen cant. 5.3.2 Instruction conditionnelle et boucles

La plupart des langages modernes poss` edent des instructions conditionnelles et des boucles tant que  d enies par des productions analogues ` a la suivante : instruction ... | tantque expression faire instruction | si expression alors instruction | si expression alors instruction sinon instruction | ... La partie de lanalyseur syntaxique correspondant ` a la r` egle pr ec edente ressemblerait ` a ceci64 : void instruction(void) { ... else if (uniteCourante == TANTQUE) { /* boucle "tant que" */ uniteCourante = uniteSuivante(); expression(); terminal(FAIRE); instruction(); } else if (uniteCourante == SI) { /* instruction conditionnelle */ uniteCourante = uniteSuivante(); expression(); terminal(ALORS); instruction(); if (uniteCourante == SINON) { uniteCourante = uniteSuivante(); instruction(); } } else ... } Commen cons par le code ` a g en erer ` a loccasion dune boucle tant que . Dans ce code on trouvera les suites dinstructions g en er ees pour lexpression et pour linstruction que la syntaxe pr evoit (ces deux suites peuvent etre tr` es longues, cela d epend de la complexit e de lexpression et de linstruction en question). Leet de cette instruction est connu : lexpression est evalu ee ; si elle est fausse (c.-` a-d. nulle) lex ecution continue apr` es le corps de la boucle ; si lexpression est vraie (c.-` a-d. non nulle) le corps de la boucle est ex ecut e, puis on revient ` a l evaluation de lexpression et on recommence tout. Notons expr1 , expr2 , ... exprn les codes-machine produits par la compilation de lexpression et instr1 , instr2 , ... instrk ceux produits par la compilation de linstruction. Pour linstruction tant que  toute enti` ere on aura donc un code comme ceci (les rep` eres dans la colonne de gauche, comme et , repr esentent les adresses des instructions) :
64 Notez que, dans le cas de linstruction conditionnelle, lutilisation dun analyseur r ecursif descendant nous a permis de faire une factorisation ` a gauche originale du d ebut commun des deux formes de linstruction si (` a propos de factorisation ` a gauche voyez eventuellement la section 3.1.3).

63

expr1 expr2 ... exprn SIFAUX instr1 instr2 ... instrk SAUT

Le point le plus dicile, ici, est de r ealiser quau moment o` u le compilateur doit produire linstruction SIFAUX , ladresse nest pas connue. Il faut donc produire quelque chose comme SIFAUX 0 et noter que la case dans laquelle est inscrit le 0 est ` a corriger ult erieurement (quand la valeur de sera connue). Ce qui donne le programme : void instruction(void) { int alpha, aCompleter; ... else if (uniteCourante == TANTQUE) { /* boucle "tant que" */ uniteCourante = uniteSuivante(); alpha = TC; expression(); genCode(SIFAUX); aCompleter = TC; genCode(0); terminal(FAIRE); instruction(); genCode(SAUT); genCode(alpha); repCode(aCompleter, TC); /* ici, beta = TC */ } else ... } o` u repCode (pour r eparer le code ) est une fonction aussi simple que genCode (cette fonction est tr` es simple parce que nous supposons que notre compilateur produit du code dans la m emoire ; elle serait consid erablement plus complexe si le code etait produit dans un chier) : void repCode(int place, int valeur) { mem[place] = valeur; } Instruction conditionnelle. Dans le m eme ordre did ees, voici le code ` a produire dans le cas de linstruction conditionnelle ` a une branche ; il nest pas dicile dimaginer ce quil faut ajouter, et o` u, dans lanalyseur montr e plus haut pour lui faire produire le code suivant : ... expr1 d ebut de linstruction si...alors... expr2 ... exprn SIFAUX instr1 instr2 ... instrk ... la suite (apr` es linstruction si...alors...) 64

Et voici le code ` a produire pour linstruction conditionnelle ` a deux branches. On note alors1 , alors2 , ... alorsk les codes-machine produits par la compilation de la premi` ere branche et sinon1 , sinon2 , ... sinonm ceux produits par la compilation de la deuxi` eme branche : expr1 expr2 ... exprn SIFAUX alors1 alors2 ... alorsk SAUT sinon1 sinon2 ... sinonm ... d ebut de linstruction si...alors...sinon...

d ebut de la premi` ere branche

d ebut de la deuxi` eme branche

5.3.3

la suite (apr` es linstruction si...alors...sinon...)

Appel de fonction

Pour nir cette galerie dexemples, examinons quel doit etre le code produit ` a loccasion de lappel dune fonction, aussi bien du c ot e de la fonction appelante que de celui de la fonction appel ee. Supposons que lon a dune part compil e la fonction suivante : entier distance(entier a, entier b) { entier x; x = a - b; si x < 0 alors x = - x; retour x; } et supposons que lon a dautre part le code (u, v, w sont les seules variables globales de ce programme) : entier u, v, w; ... entier principale() { ... u := 200 - distance(v, w / 25 - 100) * 2; ... } Voici le segment du code de la fonction principale qui est la traduction de laectation pr ec edente :

65

... EMPC 200 PILE 1 EMPG 1 EMPG 2 EMPC 25 DIV EMPC 100 SUB APPEL PILE -2 EMPC 2 MUL SUB DEPG 0 ... ... ENTREE PILE 1 EMPL -4 EMPL -3 SUB DEPL 0 EMPL 0 EMPC 0 INF SIFAUX EMPC 0 EMPL 0 SUB DEPL 0 EMPL 0 DEPL -5 SORTIE RETOUR ...

emplacement pour le r esultat de la fonction premier argument

ceci ach` eve le calcul du second argument on vire les arguments (ensuite le r esultat de la fonction est au sommet)

ceci ach` eve le calcul du membre droit de laectation

et voici le code de la fonction distance (voyez la section 5.2.4) :

allocation de lespace local (une variable)

debut de linstruction si

] ] simulation du unaire ]

le r esultat de la fonction

R ef erences
[1] Alfred Aho, Ravi Sethi, and Jerey Ullman. Compilateurs. Principes, techniques et outils. Dunod, Paris, 1991. [2] Alfred Aho and Jerey Ullman. The Theory of Parsing, Translating and Compiling. Prentice Hall Inc, 1972. [3] Andrew Appel. Modern Compiler Implementation in C. Cambridge University Press, 1998. [4] John Levine, Tony Mason, and Doug Brown. Lex & yacc. OReilly & Associates, Inc., 1990.

66

Tab. 1 Les instructions de la machine Mach 1 opcode EMPC EMPL op erande valeur adresse explication EMPiler Constante. Empile la valeur indiqu ee. EMPiler la valeur dune variable Locale. Empile la valeur de la variable d etermin ee par le d eplacement relatif ` a BEL donn e par adresse (entier relatif). DEPiler dans une variable Locale. D epile la valeur qui est au sommet et la range dans la variable d etermin ee par le d eplacement relatif ` a BEL donn e par adresse (entier relatif). EMPiler la valeur dune variable Globale. Empile la valeur de la variable d etermin ee par le d eplacement (relatif ` a BEG) donn e par adresse. DEPiler dans une variable Globale. D epile la valeur qui est au sommet et la range dans la variable d etermin ee par le d eplacement (relatif ` a BEG) donn e par adresse. EMPiler la valeur dun el ement de Tableau. D epile la valeur qui est au sommet de la pile, soit i cette valeur. Empile la valeur de la cellule qui se trouve i cases au-del` a de la variable d etermin ee par le d eplacement (relatif ` a BEG) indiqu e par adresse. DEPiler dans un el ement de Tableau. D epile une valeur v, puis une valeur i. Ensuite range v dans la cellule qui se trouve i cases au-del` a de la variable d etermin ee par le d eplacement (relatif ` a BEG) indiqu e par adresse. ADDition. D epile deux valeurs et empile le r esultat de leur addition. SOUStraction. D epile deux valeurs et empile le r esultat de leur soustraction. MULtiplication. D epile deux valeurs et empile le r esultat de leur multiplication. DIVision. D epile deux valeurs et empile le quotient de leur division euclidienne. MODulo. D epile deux valeurs et empile le reste de leur division euclidienne. D epile deux valeurs et empile 1 si elles sont egales, 0 sinon. INFerieur. D epile deux valeurs et empile 1 si la premi` ere est inf erieure ` a la seconde, 0 sinon. INFerieur ou EGal. D epile deux valeurs et empile 1 si la premi` ere est inf erieure ou egale ` a la seconde, 0 sinon. D epile une valeur et empile 1 si elle est nulle, 0 sinon. Obtient de lutilisateur un nombre et lempile ECRIre Valeur. Extrait la valeur qui est au sommet de la pile et lache Saut inconditionnel. Lex ecution continue par linstruction ayant ladresse indiqu ee. Saut conditionnel. D epile une valeur et si elle est non nulle, lex ecution continue par linstruction ayant ladresse indiqu ee. Si la valeur d epil ee est nulle, lex ecution continue normalement. Comme ci-dessus, en permutant nul et non nul. Appel de sous-programme. Empile ladresse de linstruction suivante, puis fait la m eme chose que SAUT. Retour de sous-programme. D epile une valeur et continue lex ecution par linstruction dont cest ladresse. Entr ee dans un sous-programme. Empile la valeur courante de BEL, puis copie la valeur de SP dans BEL. Sortie dun sous-programme. Copie la valeur de BEL dans SP, puis d epile une valeur et la range dans BEL. Allocation et restitution despace dans la pile. Ajoute nbreMots, qui est un entier positif ou n egatif, ` a SP La machine sarr ete.

DEPL

adresse

EMPG DEPG

adresse adresse

EMPT

adresse

DEPT

adresse

ADD SOUS MUL DIV MOD EGAL INF INFEG NON LIRE ECRIV SAUT SIVRAI

adresse adresse

SIFAUX APPEL RETOUR ENTREE SORTIE PILE STOP

adresse adresse

nbreMots

67