Licence Universitaire Professionnelle

G´nie Logiciel & Th´orie des Langages e e

Techniques et outils pour la compilation
Henri Garreta
Facult´ des Sciences de Luminy - Universit´ de la M´diterran´e e e e e Janvier 2001

Table des mati`res e
1 Introduction 1.1 Structure de principe d’un compilateur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Analyse lexicale 2.1 Expressions r´guli`res . . . . . . . . . . . . . . . . . . . . . e e 2.1.1 D´finitions . . . . . . . . . . . . . . . . . . . . . . . e 2.1.2 Ce que les expressions r´guli`res ne savent pas faire e e 2.2 Reconnaissance des unit´s lexicales . . . . . . . . . . . . . . e 2.2.1 Diagrammes de transition . . . . . . . . . . . . . . . 2.2.2 Analyseurs lexicaux programm´s  en dur  . . . . . e 2.2.3 Automates finis . . . . . . . . . . . . . . . . . . . . . 2.3 Lex, un g´n´rateur d’analyseurs lexicaux . . . . . . . . . . . e e 2.3.1 Structure d’un fichier 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´finitions . . . . . . . . . . . . . . . . . . . . . . . . e 3.1.2 D´rivations et arbres de d´rivation . . . . . . . . . . . e e 3.1.3 Qualit´s des grammaires en vue des analyseurs . . . . e 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´cursif . . . . . . . . . . . e 3.2.3 Analyse par descente r´cursive . . . . . . . . . . . . . e 3.3 Analyseurs ascendants . . . . . . . . . . . . . . . . . . . . . . 3.3.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.2 Analyse LR(k) . . . . . . . . . . . . . . . . . . . . . . 3.4 Yacc, un g´n´rateur d’analyseurs syntaxiques . . . . . . . . . e e 3.4.1 Structure d’un fichier source pour yacc . . . . . . . . . 3.4.2 Actions s´mantiques et valeurs des attributs . . . . . . e 3.4.3 Conflits et ambigu¨ es . . . . . . . . . . . . . . . . . . ıt´

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

1

4 Analyse s´mantique e 4.1 Repr´sentation et reconnaissance des types . . e 4.2 Dictionnaires (tables de symboles) . . . . . . . 4.2.1 Dictionnaire global & dictionnaire local 4.2.2 Tableau ` acc`s s´quentiel . . . . . . . . a e e 4.2.3 Tableau tri´ et recherche dichotomique . e 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 d’objets . . . . . . . . . . . . . . . 5.1.2 D’o` viennent les adresses des objets ? . . u 5.1.3 Compilation s´par´e et ´dition de liens . . e e e 5.2 La machine Mach 1 . . . . . . . . . . . . . . . . . 5.2.1 Machines ` registres et machines ` pile . . a a 5.2.2 Structure g´n´rale de la machine Mach 1 e e 5.2.3 Jeu d’instructions . . . . . . . . . . . . . 5.2.4 Compl´ments sur l’appel des fonctions . . e 5.3 Exemples de production de code . . . . . . . . . 5.3.1 Expressions arithm´tiques . . . . . . . . . e 5.3.2 Instruction conditionnelle et boucles . . . 5.3.3 Appel de fonction . . . . . . . . . . . . .

2

1

Introduction

Dans le sens le plus usuel du terme, la compilation est une transformation que l’on fait subir ` un programme a ´crit dans un langage ´volu´ pour le rendre ex´cutable. Fondamentalement, c’est une traduction : un texte ´crit e e e e e en Pascal, C, Java, etc., exprime un algorithme et il s’agit de produire un autre texte, sp´cifiant le mˆme e e algorithme dans le langage d’une machine que nous cherchons ` programmer. a En g´n´ralisant un peu, on peut dire que compiler c’est lire une suite de caract`res ob´issant ` une certaine e e e e a syntaxe, en construisant une (autre) repr´sentation de l’information que ces caract`res expriment. De ce point e e de vue, beaucoup d’op´rations apparaissent comme ´tant de la compilation ; ` la limite, la lecture d’un nombre, e e a qu’on obtient en C par une instruction comme : scanf("%f", &x); est d´j` de la compilation, puisqu’il s’agit de lire des caract`res constituant l’´criture d’une valeur selon la ea e e syntaxe des nombres d´cimaux et de fabriquer une autre repr´sentation de la mˆme information, ` savoir sa e e e a valeur num´rique. e Bien sˆr, les questions qui nous int´resseront ici seront plus complexes que la simple lecture d’un nombre. u e Mais il faut comprendre que le domaine d’application des principes et m´thodes de l’´criture de compilateurs e e contient bien d’autres choses que la seule production de programmes ex´cutables. Chaque fois que vous aurez e ae ` ´crire un programme lisant des expressions plus compliqu´es que des nombres vous pourrez tirer profit des e concepts, techniques et outils expliqu´s dans ce cours. e

1.1

Structure de principe d’un compilateur

La nature de ce qui sort d’un compilateur est tr`s variable. Cela peut ˆtre un programme ex´cutable pour e e e un processeur physique, comme un Pentium III ou un G4, ou un fichier de code pour une machine virtuelle, comme la machine Java, ou un code abstrait destin´ ` un outil qui en fera ult´rieurement du code ex´cutable, ea e e ou encore le codage d’un arbre repr´sentant la structure logique d’un programme, etc. e En entr´e d’un compilateur on trouve toujours la mˆme chose : une suite de caract`res, appel´e le texte e e e e source 1 . Voici les phases dans lesquelles se d´compose le travail d’un compilateur, du moins d’un point de vue e logique2 (voyez la figure 1) : Analyse lexicale Dans cette phase, les caract`res isol´s qui constituent le texte source sont regroup´s pour e e e former des unit´s lexicales, qui sont les mots du langage. e L’analyse lexicale op`re sous le contrˆle de l’analyse syntaxique ; elle apparaˆ comme une sorte de fonction e o ıt de  lecture am´lior´e , qui fournit un mot lors de chaque appel. e e Analyse syntaxique Alors que l’analyse lexicale reconnaˆ les mots du langage, l’analyse syntaxique en reıt connaˆ les phrases. Le rˆle principal de cette phase est de dire si le texte source appartient au langage ıt o consid´r´, c’est-`-dire s’il est correct relativement ` la grammaire de ce dernier. ee a a Analyse s´mantique La structure du texte source ´tant correcte, il s’agit ici de v´rifier certaines propri´t´s e e e ee s´mantiques, c’est-`-dire relatives ` la signification de la phrase et de ses constituants : e a a – les identificateurs apparaissant dans les expressions ont-ils ´t´ declar´s ? ee e – les op´randes ont-ils les types requis par les op´rateurs ? e e – les op´randes sont-ils compatibles ? n’y a-t-il pas des conversions ` ins´rer ? e a e – les arguments des appels de fonctions ont-ils le nombre et le type requis ? etc. G´n´ration de code interm´diaire Apr`s les phases d’analyse, certains compilateurs ne produisent pas die e e e rectement le code attendu en sortie, mais une repr´sentation interm´diaire, une sorte de code pour une e e machine abstraite. Cela permet de concevoir ind´pendamment les premi`res phases du compilateur (constie e tuant ce que l’on appelle sa face avant) qui ne d´pendent que du langage source compil´ et les derni`res e e e phases (formant sa face arri`re) qui ne d´pendent que du langage cible ; l’id´al serait d’avoir plusieurs e e e faces avant et plusieurs faces arri`re qu’on pourrait assembler librement3 . e
1 Conseil : le texte source a probablement ´t´ compos´ ` l’aide d’un ´diteur de textes qui le montre sous forme de pages faites e e ea e de plusieurs lignes mais, pour ce que nous avons ` en faire ici, prenez l’habitude de l’imaginer comme s’il ´tait ´crit sur un long et a e e mince ruban, formant une seule ligne. 2 C’est une organisation logique ; en pratique certaines de ces phases peuvent ˆtre imbriqu´es, et d’autres absentes. e e 3 De la sorte, avec n faces avant pour n langages source et m faces arri`re correspondant ` m machines cibles, on disposerait e a automatiquement de n × m compilateurs distincts. Mais cela reste, pour le moment, un fantasme d’informaticien.

3

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 sémantique aff addRéelle mulRéelle id(2) entVersRéel id(3) 60 génération de code intermédiaire tmp1 tmp2 tmp3 id(1) <te <<<EntVersRéel(60) mulRéel(id(3), tmp1) addRéel(id(2), tmp2) tmp3

optimisation du code tmp1 <- mulRéel(id(3), 60.0) id(1) <- addRéel(id(2), tmp1) génération du code final MOVF MULF ADDF MOVF id3, R0 #60.0, R0 id2, R0 R0, id1

Fig. 1 – Phases logiques de la compilation d’une instruction

Optimisation du code Il s’agit g´n´ralement ici de transformer le code afin que le programme r´sultant e e e s’ex´cute plus rapidement. Par exemple e – d´tecter l’inutilit´ de recalculer des expressions dont la valeur est d´j` connue, e e ea a e e e – transporter ` l’ext´rieur des boucles des expressions et sous-expressions dont les op´randes ont la mˆme valeur ` toutes les it´rations a e e – d´tecter, et supprimer, les expressions inutiles 4

etc. e e G´n´ration du code final Cette phase, la plus impressionnante pour le n´ophyte, n’est pas forc´ment la plus e e difficile ` r´aliser. Elle n´cessite la connaissance de la machine cible (r´elle, virtuelle ou abstraite), et a e e e notamment de ses possibilit´s en mati`re de registres, piles, etc. e e

2

Analyse lexicale

L’analyse lexicale est la premi`re phase de la compilation. Dans le texte source, qui se pr´sente comme un e e flot de caract`res, l’analyse lexicale reconnaˆ des unit´s lexicales, qui sont les  mots  avec lesquels les phrases e ıt e sont form´es, et les pr´sente ` la phase suivante, l’analyse syntaxique. e e a Les principales sortes d’unit´s lexicales qu’on trouve dans les langages de programmation courants sont : e – les caract`res sp´ciaux simples : +, =, etc. e e – les caract`res sp´ciaux doubles : <=, ++, etc. e e – les mots-cl´s : if, while, etc. e – les constantes litt´rales : 123, -5, etc. e – et les identificateurs : i, vitesse_du_vent, etc. A propos d’une unit´ lexicale reconnue dans le texte source on doit distinguer quatre notions importantes : e – l’unit´ lexicale, repr´sent´e g´n´ralement par un code conventionnel ; pour nos dix exemples +, =, <=, e e e e e ++, if, while, 123, -5, i et vitesse_du_vent, ce pourrait ˆtre, respectivement4 : PLUS, EGAL, INFEGAL, e PLUSPLUS, SI, TANTQUE, NOMBRE, NOMBRE, IDENTIF, IDENTIF. – le lex`me, qui est la chaˆ de caract`res correspondante. Pour les dix exemples pr´c´dents, les lex`mes e ıne e e e e correspondants sont : "+", "=", "<=", "++", "if", "while", "123", "-5", "i" et "vitesse_du_vent" – ´ventuellement, un attribut, qui d´pend de l’unit´ lexicale en question, et qui la compl`te. Seules les e e e e derni`res des dix unit´s pr´c´dentes ont un attribut ; pour un nombre, il s’agit de sa valeur (123, −5) ; e e e e pour un identificateur, il s’agit d’un renvoi ` une table dans laquelle sont plac´s tous les identificateurs a e rencontr´s (on verra cela plus loin). e – le mod`le qui sert ` sp´cifier l’unit´ lexicale. Nous verrons ci-apr`s des moyens formels pour d´finir rigoue a e e e e reusement les mod`les ; pour le moment nous nous contenterons de descriptions informelles comme : e – pour les caract`res sp´ciaux simples et doubles et les mots r´serv´s, le lex`me et le mod`le co¨ e e e e e e ıncident, – le mod`le d’un nombre est  une suite de chiffres, ´ventuellement pr´c´d´e d’un signe , e e e e e e e c – le mod`le d’un identificateur est  une suite de lettres, de chiffres et du caract`re ’ ’, commen¸ant par une lettre . Outre la reconnaissance des unit´s lexicales, les analyseurs lexicaux assurent certaines tˆches mineures comme e a la suppression des caract`res de d´coration (blancs, tabulations, fins de ligne, etc.) et celle des commentaires e e (g´n´ralement consid´r´s comme ayant la mˆme valeur qu’un blanc), l’interface avec les fonctions de lecture de e e ee e caract`res, ` travers lesquelles le texte source est acquis, la gestion des fichiers et l’affichage des erreurs, etc. e a Remarque. La fronti`re entre l’analyse lexicale et l’analyse syntaxique n’est pas fixe. D’ailleurs, l’analyse e lexicale n’est pas une obligation, on peut concevoir des compilateurs dans lesquels la syntaxe est d´finie ` partir e a des caract`res individuels. Mais les analyseurs syntaxiques qu’il faut alors ´crire sont bien plus complexes que e e ceux qu’on obtient en utilisant des analyseurs lexicaux pour reconnaˆ les mots du langage. ıtre Simplicit´ et efficacit´ sont les raisons d’ˆtre des analyseurs lexicaux. Comme nous allons le voir, les teche e e niques pour reconnaˆ les unit´s lexicales sont bien plus simples et efficaces que les techniques pour v´rifier la ıtre e e syntaxe.

2.1
2.1.1

Expressions r´guli`res e e
D´finitions e

Les expressions r´guli`res sont une importante notation pour sp´cifier formellement des mod`les. Pour les e e e e d´finir correctement il nous faut faire l’effort d’apprendre un peu de vocabulaire nouveau : e Un alphabet est un ensemble de symboles. Exemples : {0, 1}, {A, C, G, T }, l’ensemble de toutes les lettres, l’ensemble des chiffres, le code ASCII, etc. On notera que les caract`res blancs (c’est-`-dire les espaces, les e a
4 Dans

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

5

tabulations et les marques de fin de ligne) ne font g´n´ralement pas partie des alphabets5 . e e Une chaˆ (on dit aussi mot) sur un alphabet Σ est une s´quence finie de symboles de Σ. Exemples, ıne e respectivement relatifs aux alphabets pr´c´dents : 00011011, ACCAGTTGAAGTGGACCTTT, Bonjour, 2001. On note e e ε la chaˆ vide, ne comportant aucun caract`re. ıne e Un langage sur un alphabet Σ est un ensemble de chaˆ ınes construites sur Σ. Exemples triviaux : ∅, le langage vide, {ε}, le langage r´duit ` l’unique chaˆ vide. Des exemples plus int´ressants (relatifs aux alphabets e a ıne e pr´c´dents) : l’ensemble des nombres en notation binaire, l’ensemble des chaˆ e e ınes ADN, l’ensemble des mots de la langue fran¸aise, etc. c Si x et y sont deux chaˆ ınes, la concat´nation de x et y, not´e xy, est la chaˆ obtenue en ´crivant y e e ıne e imm´diatement apr`s x. Par exemple, la concat´nation des chaˆ e e e ınes anti et moine est la chaˆ antimoine. Si ıne x est une chaˆ on d´finit x0 = ε et, pour n > 0, xn = xn−1 x = xxn−1 . On a donc x1 = x, x2 = xx, x3 = xxx, ıne, e etc. Les op´rations sur les langages suivantes nous serviront ` d´finir les expressions r´guli`res. Soient L et M e a e e e deux langages, on d´finit : e d´nomination e l’union de L et M la concat´nation de L et M e la fermeture de Kleene de L la fermeture positive de L notation L∪M LM L∗ L+ d´finition e { 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´finition de LM on d´duit celle de Ln = LL . . . L. e e Exemples. On se donne les alphabets L = {A, B, . . . Z, a, b, . . . z}, ensemble des lettres, et C = {0, 1, . . . 9}, ensemble des chiffres. En consid´rant qu’un caract`re est la mˆme chose qu’une chaˆ de longueur un, on peut e e e ıne voir L et C comme des langages, form´s de chaˆ e ınes de longueur un. Dans ces conditions : – L ∪ C est l’ensemble des lettres et des chiffres, – LC est l’ensemble des chaˆ ınes form´es d’une lettre suivie d’un chiffre, e – L4 est l’ensemble des chaˆ ınes de quatre lettres, – L∗ est l’ensemble des chaˆ ınes faites d’un nombre quelconque de lettres ; ε en fait partie, – C + est l’ensemble des chaˆ ınes de chiffres comportant au moins un chiffre, – L(L ∪ C)∗ est l’ensemble des chaˆ ınes de lettres et chiffres commen¸ant par une lettre. c ´ ` Expression reguliere. Soit Σ un alphabet. Une expression r´guli`re r sur Σ est une formule qui d´finit e e e un langage L(r) sur Σ, de la mani`re suivante : e 1. ε est une expression r´guli`re qui d´finit le langage {ε} e e e 2. Si a ∈ Σ, alors a est une expression r´guli`re qui d´finit le langage6 {a} e e e 3. Soient x et y deux expressions r´guli`res, d´finissant les langages L(x) et L(y). Alors e e e – (x)|(y) est une expression r´guli`re d´finissant le langage L(x) ∪ L(y) e e e – (x)(y) est une expression r´guli`re d´finissant le langage L(x)L(y) e e e – (x)∗ est une expression r´guli`re d´finissant le langage (L(x))∗ e e e – (x) est une expression r´guli`re d´finissant le langage L(x) e e e La derni`re r`gle ci-dessus signifie qu’on peut encadrer une expression r´guli`re par des parenth`ses sans e e e e e changer le langage d´fini. D’autre part, les parenth`ses apparaissant dans les r`gles pr´c´dentes peuvent souvent e e e e e ˆtre omises, en fonction des op´rateurs en pr´sence : il suffit de savoir que les op´rateurs ∗, concat´nation et | e e e e e sont associatifs ` gauche, et v´rifient a e priorit´ ( ∗ ) > priorit´ ( concat´nation ) > priorit´ ( | ) e e e e Ainsi, on peut ´crire l’expression r´guli`re oui au lieu de (o)(u)(i) et oui|non au lieu de (oui)|(non), e e e mais on ne doit pas ´crire oui∗ au lieu de (oui)∗ . e
5 Il en d´coule que les unit´s lexicales, sauf mesures particuli`res (apostrophes, quillemets, etc.), ne peuvent pas contenir des e e e caract`res blancs. D’autre part, la plupart des langages autorisent les caract`res blancs entre les unit´s lexicales. e e e 6 On prendra garde ` l’abus de langage qu’on fait ici, en employant la mˆme notation pour le caract`re a, la chaˆ a e e ıne a et l’expression r´guli`re a. En principe, le contexte permet de savoir de quoi on parle. e e

6

´ ´ ` Definitions regulieres. Les expressions r´guli`res se construisent ` partir d’autres expressions r´guli`res ; e e a e e cela am`ne ` des expressions passablement touffues. On les all`ge en introduisant des d´finitions r´guli`res qui e a e e e e permettent de donner des noms ` certaines expressions en vue de leur r´utilisation. On ´crit donc a e e d1 → r1 d2 → r2 ... dn → rn o` chaque di est une chaˆ sur un alphabet disjoint de Σ 7 , distincte de d1 , d2 , . . . di−1 , et chaque ri une u ıne expression r´guli`re sur Σ ∪ {d1 , d2 , . . . di−1 }. e e Exemple. Voici quelques d´finitions r´guli`res, et notamment celles de identificateur et nombre, qui d´finissent e e e e les identificateurs et les nombres du langage Pascal : lettre → A | B | . . . | Z | a | b | . . . | z chiffre → 0 | 1 | . . . | 9 identificateur → lettre ( lettre | chiffre )∗ chiffres → chiffre chiffre ∗ fraction-opt → . chiffres | ε exposant-opt → ( E (+ | - | ε) chiffres ) | ε nombre → chiffres fraction-opt exposant-opt ´ ´ Notations abregees. Pour all´ger certaines ´critures, on compl`te la d´finition des expressions r´guli`res e e e e e e en ajoutant les notations suivantes : – soit x une expression r´guli`re, d´finissant le langage L(x) ; alors (x)+ est une expression r´guli`re, qui e e e e e d´finit le langage (L(x))+ , e – soit x une expression r´guli`re, d´finissant le langage L(x) ; alors (x)? est une expression r´guli`re, qui e e e e e d´finit le langage L(x) ∪ { ε }, e – si c1 , c2 , . . . ck sont des caract`res, l’expressions r´guli`re c1 |c2 | . . . |ck peut se noter [c1 c2 . . . ck ], e e e – ` l’int´rieur d’une paire de crochets comme ci-dessus, l’expression c1 –c2 d´signe la s´quence de tous les a e e e caract`res c tels que c1 ≤ c ≤ c2 . e Les d´finitions de lettre et chiffre donn´es ci-dessus peuvent donc se r´´crire : e e ee lettre → [A–Za–z] chiffre → [0–9] 2.1.2 Ce que les expressions r´guli`res ne savent pas faire e e

Les expressions r´guli`res sont un outil puissant et pratique pour d´finir les unit´s lexicales, c’est-`-dire e e e e a les constituants ´l´mentaires des programmes. Mais elles se prˆtent beaucoup moins bien ` la sp´cification de ee e a e constructions de niveau plus ´lev´, car elles deviennent rapidement d’une trop grande complexit´. e e e De plus, on d´montre qu’il y a des chaˆ e ınes qu’on ne peut pas d´crire par des expressions r´guli`res. Par e e e exemple, le langage suivant (suppos´ infini) e { a, (a), ((a)), (((a))), . . . } ne peut pas ˆtre d´fini par des expressions r´guli`res, car ces derni`res ne permettent pas d’assurer qu’il y a dans e e e e e une expression de la forme (( . . . ((a)) . . . )) autant de parenth`ses ouvrantes que de parenth`ses fermantes. e e On dit que les expressions r´guli`res  ne savent pas compter . e e Pour sp´cifier ces structures ´quilibr´es, si importantes dans les langages de programmation (penser aux e e e parenth`ses dans les expressions arithm´tiques, les crochets dans les tableaux, begin...end, {...}, if...then..., etc.) e e nous ferons appel aux grammaires non contextuelles, expliqu´es ` la section 3.1. e a
7 On

assure souvent la s´paration entre Σ∗ et les noms des d´finitions r´guli`res par des conventions typographiques. e e e e

7

2.2

Reconnaissance des unit´s lexicales e

Nous avons vu comment sp´cifier les unit´s lexicales ; notre probl`me maintenant est d’´crire un programme e e e e qui les reconnaˆ dans le texte source. Un tel programme s’appelle un analyseur lexical. ıt Dans un compilateur, le principal client de l’analyseur lexical est l’analyseur syntaxique. L’interface entre ces deux analyseurs est une fonction int uniteSuivante(void) 8 , qui renvoie ` chaque appel l’unit´ lexicale suivante a e trouv´e dans le texte source. e Cela suppose que l’analyseur lexical et l’analyseur syntaxique partagent les d´finitions des constantes convene tionnelles d´finissant les unit´s lexicales. Si on programme en C, cela veut dire que dans les fichiers sources des e e deux analyseurs on a inclus un fichier d’entˆte (fichier  .h ) comportant une s´rie de d´finitions comme9 : e e e #define #define #define #define #define etc. IDENTIF NOMBRE SI ALORS SINON 1 2 3 4 5

Cela suppose aussi que l’analyseur lexical et l’analyseur syntaxique partagent ´galement une variable globale e contenant le lex`me correspondant ` la derni`re unit´ lexicale reconnue, ainsi qu’une variable globale contenant e a e e le (ou les) attribut(s) de l’unit´ lexicale courante, lorsque cela est pertinent, et notamment lorsque l’unit´ lexicale e e est NOMBRE ou IDENTIF. On se donnera, du moins dans le cadre de ce cours, quelques  r`gles du jeu  suppl´mentaires : e e – l’analyseur lexical est  glouton  : chaque lex`me est le plus long possible10 ; e e e e – seul l’analyseur lexical acc`de au texte source. L’analyseur syntaxique n’acquiert ses donn´es d’entr´e autrement qu’` travers la fonction uniteSuivante ; a – l’analyseur lexical acquiert le texte source un caract`re ` la fois. Cela est un choix que nous faisons ici ; e a d’autres choix auraient ´t´ possibles, mais nous verrons que les langages qui nous int´ressent permettent ee e de travailler de cette mani`re. e 2.2.1 Diagrammes de transition

Pour illustrer cette section nous allons nous donner comme exemple le probl`me de la reconnaissance e des unit´s lexicales INFEG, DIFF, INF, EGAL, SUPEG, SUP, IDENTIF, respectivement d´finies par les expressions e e r´guli`res <=, <>, <, =, >=, > et lettre(lettre|chiffre)∗ , lettre et chiffre ayant leurs d´finitions d´j` vues. e e e ea Les diagrammes de transition sont une ´tape pr´paratoire pour la r´alisation d’un analyseur lexical. Au fur e e e et ` mesure qu’il reconnaˆ une unit´ lexicale, l’analyseur lexical passe par divers ´tats. Ces ´tats sont num´rot´s a ıt e e e e e et repr´sent´s dans le diagramme par des cercles. e e De chaque ´tat e sont issues une ou plusieurs fl`ches ´tiquet´es par des caract`res. Une fl`che ´tiquet´e par e e e e e e e e c relie e ` l’´tat e1 dans lequel l’analyseur passera si, alors qu’il se trouve dans l’´tat e, le caract`re c est lu dans a e e e le texte source. Un ´tat particulier repr´sente l’´tat initial de l’analyseur ; on le signale en en faisant l’extr´mit´ d’une fl`che e e e e e e ´tiquet´e debut. e e Des doubles cercles identifient les ´tats finaux, correspondant ` la reconnaissance compl`te d’une unit´ e a e e lexicale. Certains ´tats finaux sont marqu´s d’une ´toile : cela signifie que la reconnaissance s’est faite au prix e e e de la lecture d’un caract`re au-del` de la fin du lex`me11 . e a e Par exemple, la figure 2 montre les diagrammes traduisant la reconnaissance des unit´s lexicales INFEG, e DIFF, INF, EGAL, SUPEG, SUP et IDENTIF. Un diagramme de transition est dit non d´terministe lorsqu’il existe, issues d’un mˆme ´tat, plusieurs fl`ches e e e e ´tiquet´es par le mˆme caract`re, ou bien lorsqu’il existe des fl`ches ´tiquet´es par la chaˆ vide ε. Dans le cas e e e e e e e ıne
on a employ´ l’outil lex pour fabriquer l’analyser lexical, cette fonction s’appelle plutˆt yylex ; le lex`me est alors point´ par e o e e la variable globale yytext et sa longueur est donn´e par la variable globale yylen. Tout cela est expliqu´ ` la section 2.3. e ea 9 Peu importent les valeurs num´riques utilis´es, ce qui compte est qu’elles soient distinctes. e e 10 Cette r`gle est peu mentionn´e dans la litt´rature, pourtant elle est fondamentale. C’est grˆce ` elle que 123 est reconnu comme e e e a a un nombre, et non plusieurs, vitesseV ent comme un seul identificateur, et f orce comme un identificateur, et non pas comme un mot r´serv´ suivi d’un identificateur. e e 11 Il faut ˆtre attentif ` ce caract`re, car il est susceptible de faire partie de l’unit´ lexicale suivante, surtout s’il n’est pas blanc. e a e e
8 Si

8

début

0

<

1

= >
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´rateurs de comparaison et les identificateurs e

contraire, le diagramme est dit d´terministe. Il est clair que le diagramme de la figure 2 est d´terministe. Seuls e e les diagrammes d´terministes nous int´resseront dans le cadre de ce cours. e e 2.2.2 Analyseurs lexicaux programm´s  en dur  e

Les diagrammes de transition sont une aide importante pour l’´criture d’analyseurs lexicaux. Par exemple, e a ` partir du diagramme de la figure 2 on peut obtenir rapidement un analyseur lexical reconnaissant les unit´s e INFEG, DIFF, INF, EGAL, SUPEG, SUP et IDENTIF. Auparavant, nous apportons une l´g`re modification ` nos diagrammes de transition, afin de permettre que e e a les unit´s lexicales soient s´par´es par un ou plusieurs blancs12 . La figure 3 montre le (d´but du) diagramme e e e e modifi´13 . e
blanc début < = > lettre

0

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

Fig. 3 – Ignorer les blancs devant une unit´ lexicale e

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`re de tabulation ou une marque de fin de ligne e que cela revient ` modifier toutes les expressions r´guli`res, en rempla¸ant  <=  par  (blanc)∗ <= ,  <  par  (blanc)∗ < , a e e c

9

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´c´dent on utilise des fonctions auxiliaires, dont voici une version simple : e e 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 l’efficacit´ de ces fonctions, au d´triment de leur s´curit´ d’utilisation, en en e e e e 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`res de prendre en charge la restitution d’un caract`re lu en trop (notre fonction delireCar ). e e Si on dispose de la biblioth`que standard C on peut utiliser la fonction ungetc : e void delireCar(char c) { ungetc(c, stdin); } char lireCar(void) { return getc(stdin); } Une autre mani`re de faire permet de se passer de la fonction ungetc. Pour cela, on g`re une variable globale e e contenant, quand il y a lieu, le caract`re lu en trop (il n’y a jamais plus d’un caract`re lu en trop). D´claration : e e e 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; } ´ ´ Reconnaissance des mots reserves. Les mots r´serv´s appartiennent au langage d´fini par l’expression e e e r´guli`re lettre(lettre|chiffre)∗ , tout comme les identificateurs. Leur reconnaissance peut donc se traiter de deux e e mani`res : e – soit on incorpore les mots r´serv´s au diagrammes de transition, ce qui permet d’obtenir un analyseur e e tr`s efficace, mais au prix d’un travail de programmation plus important, car les diagrammes de transition e deviennent tr`s volumineux14 , e – soit on laisse l’analyseur traiter de la mˆme mani`re les mots r´serv´s et les identificateurs puis, quand la e e e e reconnaissance d’un  identificateur-ou-mot-r´serv´  est termin´e, on recherche le lex`me dans une table e e e e pour d´terminer s’il s’agit d’un identificateur ou d’un mot r´serv´. e e e Dans les analyseurs lexicaux  en dur  on utilise souvent la deuxi`me m´thode, plus facile ` programmer. e e a On se donne donc une table de mots r´serv´s : e e struct { char *lexeme; int uniteLexicale; } motRes[] = { { "si", SI }, { "alors", ALORS }, { "sinon", SINON }, ... }; int nbMotRes = sizeof motRes / sizeof motRes[0]; puis on modifie de la mani`re suivante la partie concernant les identificateurs de la fonction uniteSuivante : e ... else if (estLettre(c)) { lonLex = 0; lexeme[lonLex++] = c;
14 Nous

/* etat =

9 */

utiliserons cette solution quand nous ´tudierons l’outil lex, ` la section 2.3 e a

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 finis

Un automate fini est d´fini par la donn´e de e e – un ensemble fini d’´tats E, e – un ensemble fini de symboles (ou alphabet) d’entr´e Σ, e – une fonction de transition, transit : E × Σ → E, – un ´tat ε0 distingu´, appel´ ´tat initial, e e ee – un ensemble d’´tats F , appel´s ´tats d’acceptation ou ´tats finaux. e e e e Un automate peut ˆtre repr´sent´ graphiquement par un graphe o` les ´tats sont figur´s par des cercles (les e e e u e e ´tats finaux par des cercles doubles) et la fonction de transition par des fl`ches ´tiquet´es par des caract`res : e e e e e si transit(e1 , c) = e2 alors le graphe a une fl`che ´tiquet´e par le caract`re c, issue de e1 et aboutissant ` e2 . e e e e a Un tel graphe est exactement ce que nous avons appel´ diagramme de transition ` la section 2.2.1 (voir la e a figure 2). Si on en reparle ici c’est qu’on peut en d´duire un autre style d’analyseur lexical, assez diff´rent de ce e e que nous avons appel´ analyseur programm´  en dur . e e On dit qu’un automate fini accepte une chaˆ d’entr´e s = c1 c2 . . . ck si et seulement si il existe dans le ıne e graphe de transition un chemin joignant l’´tat initial e0 ` un certain ´tat final ek , compos´ de k fl`ches ´tiquet´es e a e e e e e par les caract`res c1 , c2 , . . . ck . e Pour transformer un automate fini en un analyseur lexical il suffira donc d’associer une unit´ lexicale ` e a chaque ´tat final et de faire en sorte que l’acceptation d’une chaˆ produise comme r´sultat l’unit´ lexicale e ıne e e associ´e ` l’´tat final en question. e a e Autrement dit, pour programmer un analyseur il suffira maintenant d’impl´menter la fonction transit ce e qui, puisqu’elle est d´finie sur des ensembles finis, pourra se faire par une table ` double entr´e. Pour les e a e diagrammes des figures 2 et 3 cela donne la table suivante (les ´tats finaux sont indiqu´s en gras, leurs lignes e e ont ´t´ supprim´es) : ee e ’ ’ 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 chiffre erreur 4∗ 8∗ 9 autre erreur 4∗ 8∗ 10∗

0 1 6 9

On obtiendra un analyseur peut-ˆtre plus encombrant que dans la premi`re mani`re, mais certainement plus e e e rapide puisque l’essentiel du travail de l’analyseur se r´duira ` r´p´ter  bˆtement  l’action etat = transit[etat][lireCar( e a e e e jusqu’` tomber sur un ´tat final. Voici ce programme : a e #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´ par les ´tats, est d´fini par e e e – final[e] = 0 si e n’est pas un ´tat final (vu comme un bool´en, final[e] est faux), e e – final[e] = U + 1 si e est final, sans ´toile et associ´ ` l’unit´ lexicale U (en tant que bool´en, final[e] e ea e e est vrai, car les unit´s lexicales sont num´rot´es au moins ` partir de z´ro), e e e a e – final[e] = −(U + 1) si e est final, ´toil´ et associ´ ` l’unit´ lexicale U (en tant que bool´en, final[e] e e ea e e est encore vrai). Enfin, voici comment les tableaux transit et final devraient ˆtre initialis´s pour correspondre aux diae e grammes des figures 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´ un ´tat suppl´mentaire, ayant le num´ro NBR ETATS, qui correspond ` la mise en erreur de l’analyseur lexical, e e e e a et une unit´ lexicale ERREUR pour signaler cela e

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´n´rateur d’analyseurs lexicaux e e

Les analyseurs lexicaux bas´s sur des tables de transitions sont les plus efficaces... une fois la table de e transition construite. Or, la construction de cette table est une op´ration longue et d´licate. e e Le programme lex 16 fait cette construction automatiquement : il prend en entr´e un ensemble d’expressions e r´guli`res et produit en sortie le texte source d’un programme C qui, une fois compil´, est l’analyseur lexical e e e correspondant au langage d´fini par les expressions r´guli`res en question. e e e Plus pr´cis´ment (voyez la figure 4), lex produit un fichier source C, nomm´ lex.yy.c, contenant la d´finition e e e e de la fonction int yytext(void), qui est l’exacte homologue de notre fonction uniteSuivante de la section 2.2.2 : un programme appelle cette fonction et elle renvoie une unit´ lexicale reconnue dans le texte source. e

expressions régulières

lex

lex.yy.c

lex.yy.c gcc autres modules monProg

source à analyser

monProg

résultat de l'analyse

Fig. 4 – Utilisation de lex

2.3.1

Structure d’un fichier source pour lex

En lisant cette section, souvenez-vous de ceci : lex ´crit un fichier source C. Ce fichier est fait de trois sortes e d’ingr´dients : e – des tables garnies de valeurs calcul´es ` partir des expressions r´guli`res fournies, e a e e a – des morceaux de code C invariable, et notamment le  moteur  de l’automate, c’est-`-dire la boucle qui r´p`te inlassablement etat ← transit (etat, caractere), e e e e a – des morceaux de code C, trouv´s dans le fichier source lex et recopi´s tels quels, ` l’endroit voulu, dans le fichier produit. Un fichier source pour lex doit avoir un nom qui se termine par  .l . Il est fait de trois sections, d´limit´es e e par deux lignes r´duites17 au symbole %% : e %{ d´clarations pour le compilateur C e %} d´finitions r´guli`res e e e
est un programme gratuit qu’on trouve dans le syst`me UNIX pratiquement depuis ses d´buts. De nos jours on utilise e e souvent flex, une version am´lior´e de lex qui appartient ` la famille GNU. e e a 17 Notez que les symboles %%, %{ et %}, quand ils apparaissent, sont ´crits au d´but de la ligne, aucun blanc ne les pr´c`de. e e e e
16 Lex

14

%% r`gles e %% fonctions C suppl´mentaires e La partie  d´clarations pour le compilateur C  et les symboles %{ et %} qui l’encadrent peuvent ˆtre omis. e e Quand elle est pr´sente, cette partie se compose de d´clarations qui seront simplement recopi´es au d´but du e e e e fichier produit. En plus d’autres choses, on trouve souvent ici une directive #include qui produit l’inclusion du fichier  .h  contenant les d´finitions des codes conventionnels des unit´s lexicales (INFEG, INF, EGAL, etc.). e e La troisi`me section  fonctions C suppl´mentaires  peut ˆtre absente ´galement (le symbole %% qui la s´pare e e e e e de la deuxi`me section peut alors ˆtre omis). Cette section se compose de fonctions C qui seront simplement e e recopi´es ` la fin du fichier produit. e a ´ ´ ` Definitions regulieres. Les d´finitions r´guli`res sont de la forme e e e identificateur expressionR´guli`re e e o` identificateur est ´crit au d´but de la ligne (pas de blancs avant) et s´par´ de expressionR´guli`re par des u e e e e e e blancs. Exemples : lettre chiffre [A-Za-z] [0-9]

Les identificateurs ainsi d´finis peuvent ˆtre utilis´s dans les r`gles et dans les d´finitions subs´quentes ; il e e e e e e faut alors les encadrer par des accolades. Exemples : lettre chiffre alphanum %% {lettre}{alphanum}* {chiffre}+("."{chiffre}+)? expressionR´guli`re e e { return IDENTIF; } { return NOMBRE; } [A-Za-z] [0-9] {lettre}|{chiffre}

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

La r`gle e expressionR´guli`re e e { action } signifie  ` la fin de la reconnaissance d’une chaˆ du langage d´fini par expressionR´guli`re ex´cutez action . a ıne e e e e Le traitement par lex d’une telle r`gle consiste donc ` recopier l’action indiqu´e ` un certain endroit de la fonction e a e a yylex 18 . Dans les exemples ci-dessus, les actions ´tant toutes de la forme  return unite , leur signification e est claire : quand une chaˆ du texte source est reconnue, la fonction yylex se termine en rendant comme ıne r´sultat l’unit´ lexicale reconnue. Il faudra appeler de nouveau cette fonction pour que l’analyse du texte source e e reprenne. A la fin de la reconnaissance d’une unit´ lexicale la chaˆ accept´e est la valeur de la variable yytext, de e ıne e type chaˆ de caract`res19 . Un caract`re nul indique la fin de cette chaˆ ; de plus, la variable enti`re yylen ıne e e ıne e donne le nombre de ses caract`res. Par exemple, la r`gle suivante reconnaˆ les nombres entiers et en calcule la e e ıt valeur dans une variable yylval :
parce que ces actions sont copi´es dans une fonction qu’on a le droit d’y utiliser l’instruction return. e variable yytext est d´clar´e dans le fichier produit par lex ; il vaut mieux ne pas chercher ` y faire r´f´rence dans d’autres e e a ee fichiers, car il n’est pas sp´cifi´ laquelle des d´clarations  extern char *yytext  ou  extern char yytext[]  est pertinente. e e e
19 La 18 C’est

15

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

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

´ ` Expressions regulieres. Les expressions r´guli`res accept´es par lex sont une extension de celles d´finies e e e e a ` la section 2.1. Les m´ta-caract`res pr´c´demment introduits, c’est-`-dire (, ), |, ∗, +, ?, [, ] et − ` l’int´rieur e e e e a a e des crochets, sont l´gitimes dans lex et y ont le mˆme sens. En outre, on dispose de ceci (liste non exhaustive) : e e – un point . signifie un caract`re quelconque, diff´rent de la marque de fin de ligne, e e – on peut encadrer par des guillemets un caract`re ou une chaˆ pour ´viter que les m´ta-caract`res qui s’y e ıne, e e e trouvent soient interpr´t´s comme tels. Par exemple, "." signifie le caract`re . (et non pas un caract`re ee e e quelconque), " " signifie un blanc, "[a-z]" signifie la chaˆ [a-z], etc., ıne D’autre part, on peut sans inconv´nient encadrer par des guillemets un caract`re ou une chaˆ qui n’en e e ıne avaient pas besoin, – l’expression [^caract`res] signifie : tout caract`re n’appartenant pas ` l’ensemble d´fini par [caract`res], e e a e e – l’expression  ^expressionR´guli`re  signifie : toute chaˆ reconnue par expressionR´guli`re ` la condition e e ıne e e a qu’elle soit au d´but d’une ligne, e – l’expression  expressionR´guli`re$  signifie : toute chaˆ reconnue par expressionR´guli`re ` la condition e e ıne e e a qu’elle soit ` la fin d’une ligne. a Attention. Il faut ˆtre tr`s soigneux en ´crivant les d´finitions et les r`gles dans le fichier source lex. En e e e e e effet, tout texte qui n’est pas exactement ` sa place (par exemple une d´finition ou une r`gle qui ne commencent a e e pas au d´but de la ligne) sera recopi´ dans le fichier produit par lex. C’est un comportement voulu, parfois utile, e e mais qui peut conduire ` des situations confuses. a ´ Echo du texte analyse. L’analyseur lexical produit par lex prend son texte source sur l’entr´e standard20 e et l’´crit, avec certaines modifications, sur la sortie standard. Plus pr´cisement : e e – tous les caract`res qui ne font partie d’aucune chaˆ reconnue sont recopi´s sur la sortie standard (ils e ıne e  traversent  l’analyseur lexical sans en ˆtre affect´s), e e – une chaˆ accept´e au titre d’une expression r´guli`re n’est pas recopi´e sur la sortie standard. ıne e e e e Bien entendu, pour avoir les chaˆ ınes accept´es dans le texte ´crit par l’analyseur il suffit de le pr´voir dans e e e l’action correspondante. Par exemple, la r`gle suivante reconnaˆ les identificateurs et fait en sorte qu’ils figurent e ıt 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ˆ tr`s fr´quemment dans les actions. On peut l’abr´ger en ECHO : ıt e e e

Voici le texte source pour cr´er l’analyseur lexical d’un langage comportant les nombres et les identificateurs e d´finis comme d’habitude, les mots r´serv´s si, alors, sinon, tantque, faire et rendre et les op´rateurs e e e e doubles ==, !=, <= et >=. Les unit´s lexicales correspondantes sont respectivement repr´sent´es par les constantes e e e conventionnelles IDENTIF, NOMBRE, SI, ALORS, SINON, TANTQUE, FAIRE, RENDRE, EGAL, DIFF, INFEG, SUPEG, d´finies dans le fichier unitesLexicales.h. e Pour les op´rateurs simples on d´cide que tout caract`re non reconnu par une autre expression r´guli`re est e e e e e une unit´ lexicale, et qu’elle est repr´sent´e par son propre code ASCII21 . Pour ´viter des collisions entre ces e e e e codes ASCII et les unit´s lexicales nomm´es, on donne ` ces derni`res des valeurs sup´rieures ` 255. e e a e e a 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´faut en donnant une valeur ` la variable yyin, avant le premier appel de yylex ; par e a exemple : yyin = fopen(argv[1], "r") ; 21 Cela veut dire qu’on s’en remet au  client  de l’analyseur lexical, c’est-`-dire l’analyseur syntaxique, pour s´parer les op´rateurs a e e pr´vus par le langage des caract`res sp´ciaux qui n’en sont pas. Ou, dit autrement, que nous transformons l’erreur  caract`re e e e e ill´gal , ` priori lexicale, en une erreur syntaxique. e a

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´monstratif) : e #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 l’analyseur : si x == 123 alors y = 0; Cr´ation d’un ex´cutable et essai sur le texte pr´c´dent (rappelons que lex s’appelle flex dans le monde e e e e Linux) ; $ est le prompt du syst`me : e $ 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ˆ dans notre fichier source pour lex est appel´e lorsque l’analyseur ıt e rencontre la fin du fichier ` analyser22 . Outre d’´ventuelles actions utiles dans telle ou telle application para e ticuli`re, cette fonction doit rendre une valeur non nulle pour indiquer que le flot d’entr´e est d´finitivement e e e ´puis´, ou bien ouvrir un autre flot d’entr´e. e e e 2.3.3 Autres utilisations de lex

Ce que le programme g´n´r´ par lex fait n´cessairement, c’est reconnaˆ e ee e ıtre les chaˆ ınes du langage d´fini e par les expressions r´guli`res donn´es. Quand une telle reconnaissance est accomplie, on n’est pas oblig´ de e e e e renvoyer une unit´ lexicale pour signaler la chose ; on peut notamment d´clencher l’action qu’on veut et ne pas e e retourner ` la fonction appelante. Cela permet d’utiliser lex pour effectuer d’autres sortes de programmes que a 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´c´dent exploite le fait que tous les caract`res qui ne font pas partie d’une chaˆ reconnue e e e ıne sont recopi´s sur la sortie ; ainsi, la plupart des caract`res du texte donn´ seront recopi´s tels quels. Les chaˆ e e e e ınes reconnues sont d´finies par l’expression r´guli`re e e e [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´duite ` { return 1 ; }, est fournie et on n’a pas ` e a a s’en occuper.

18

– une suite d’au moins un chiffre, – ´ventuellement, un point suivi d’un nombre quelconque de chiffres, e – ´ventuellement, un nombre quelconque de blancs (espaces, tabulations, fins de ligne), e – un F majuscule obligatoire, – ´ventuellement, la chaˆ rancs ou un point (ainsi,  F ,  F.  et  Francs  sont tous trois accept´s), e ıne e – enfin, un blanc obligatoire. Lorsqu’une chaˆ s’accordant ` cette syntaxe est reconnue, comme  99.50 Francs , la fonction atof ıne a obtient la valeur que [le d´but de] cette chaˆ repr´sente. Il suffit alors de mettre dans le texte de sortie le e ıne e r´sultat de la division de cette valeur par le taux ad´quat ; soit, ici,  15.17 EUR . e e

3
3.1

Analyse syntaxique
Grammaires non contextuelles

Les langages de programmation sont souvent d´finis par des r`gles r´cursives, comme :  on a une expression e e e en ´crivant successivement un terme, ’+’ et une expression  ou  on obtient une instruction en ´crivant ` la e e a suite si, une expression, alors, une instruction et, ´ventuellement, sinon et une instruction . Les grammaires e non contextuelles sont un formalisme particuli`rement bien adapt´ ` la description de telles r`gles. e ea e 3.1.1 D´finitions e

Une grammaire non contextuelle, on dit parfois grammaire BNF (pour Backus-Naur form23 ), est un quadruplet G = (VT , VN , S0 , P ) form´ de e – un ensemble VT de symboles terminaux, – un ensemble VN de symboles non terminaux, – un symbole S0 ∈ VN particulier, appel´ symbole de d´part ou axiome, e e – un ensemble P de productions, qui sont des r`gles de la forme e S → S1 S2 . . . Sk avec S ∈ VN et Si ∈ VN ∪ VT Compte tenu de l’usage que nous en faisons dans le cadre de l’´criture de compilateurs, nous pouvons e expliquer ces ´l´ments de la mani`re suivante : ee e 1. Les symboles terminaux sont les symboles ´l´mentaires qui constituent les chaˆ ee ınes du langage, les phrases. Ce sont donc les unit´s lexicales, extraites du texte source par l’analyseur lexical (il faut se rappeler que e l’analyseur syntaxique ne connaˆ pas les caract`res dont le texte source est fait, il ne voit ce dernier que ıt e comme une suite d’unit´s lexicales). e 2. Les symboles non terminaux sont des variables syntaxiques d´signant des ensembles de chaˆ e ınes de symboles terminaux. e e 3. Le symbole de d´part est un symbole non terminal particulier qui d´signe le langage en son entier. 4. Les productions peuvent ˆtre interpr´t´es de deux mani`res : e ee e – comme des r`gles d’´criture (on dit plutˆt de r´´criture), permettant d’engendrer toutes les chaˆ e e o ee ı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`res possibles] il fait produire un S1 [de toutes les mani`res possibles] suivi e e d’un S2 [de toutes les mani`res possibles] suivi d’un . . . suivi d’un Sk [de toutes les mani`res possibles] , e e e – comme des r`gles d’analyse, on dit aussi reconnaissance. La production S → S1 S2 . . . Sk se lit alors  pour reconnaˆ un S, dans une suite de terminaux donn´e, il faut reconnaˆ un S1 suivi d’un S2 ıtre e ıtre suivi d’un . . . suivi d’un Sk  La d´finition d’une grammaire devrait donc commencer par l’´num´ration des ensembles VT et VN . En e e e pratique on se limite ` donner la liste des productions, avec une convention typographique pour distinguer les a symboles terminaux des symboles non terminaux, et on convient que : – VT est l’ensemble de tous les symboles terminaux apparaissant dans les productions, – VN est l’ensemble de tous les symboles non terminaux apparaissant dans les productions, – le symbole de d´part est le membre gauche de la premi`re production. e e En outre, on all`ge les notations en d´cidant que si plusieurs productions ont le mˆme membre gauche e e e
23 J.

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

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ˆ en italiques repr´sente un symbole non terminal, ıne e – une chaˆ en caract`res t´l´type ou "entre guillemets", repr´sente un symbole terminal. ıne e e e e A titre d’exemple, voici la grammaire G1 d´finissant le langage dont les chaˆ e ınes sont les expressions arithm´tiques form´es avec des nombres, des identificateurs et les deux op´rateurs + et *, comme  60 * vitesse e e e + 200 . Suivant notre convention, les symboles non terminaux sont expression, terme et facteur ; le symbole de d´part est expression : e expression → expression "+" terme | terme terme → terme "*" facteur | facteur facteur → nombre | identificateur | "(" expression ")" 3.1.2 D´rivations et arbres de d´rivation e e (G1 )

´ Derivation. Le processus par lequel une grammaire d´finit un langage s’appelle d´rivation. Il peut ˆtre e e e formalis´ de la mani`re suivante : e e Soit G = (VT , VN , S0 , P ) une grammaire non contextuelle, A ∈ VN un symbole non terminal et γ ∈ (VT ∪VN )∗ une suite de symboles, tels qu’il existe dans P une production A → γ. Quelles que soient les suites de symboles α et β, on dit que αAβ se d´rive en une ´tape en la suite αγβ ce qui s’´crit e e e αAβ ⇒ αγβ Cette d´finition justifie la d´nomination grammaire non contextuelle (on dit aussi grammaire ind´pendante e e e du contexte ou context free). En effet, dans la suite αAβ les chaˆ ınes α et β sont le contexte du symbole A. Ce que cette d´finition dit, c’est que le symbole A se r´´crit dans la chaˆ γ quel que soit le contexte α, β dans le e ee ıne lequel A apparaˆ ıt. Si α0 ⇒ α1 ⇒ · · · ⇒ αn on dit que α0 se d´rive en αn en n ´tapes, et on ´crit e e e α0 ⇒ αn . Enfin, si α se d´rive en β en un nombre quelconque, ´ventuellement nul, d’´tapes on dit simplement que α e e e se d´rive en β et on ´crit e e α ⇒ β. Soit G = {VT , VN , S0 , P } une grammaire non contextuelle ; le langage engendr´ par G est l’ensemble des e chaˆ ınes de symboles terminaux qui d´rivent de S0 : e L (G) = w ∈ VT ∗ | S0 ⇒ w Si w ∈ L(G) on dit que w est une phrase de G. Plus g´n´ralement, si α ∈ (VT ∪ VN )∗ est tel que S0 ⇒ α e e 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´rons la chaˆ "60 * vitesse + 200" qui, une fois lue par l’analyseur lexical, se pr´sente ainsi : e ıne e ∗ w = ( nombre "*" identificateur "+" nombre ). Nous avons expression ⇒ w, c’est ` dire w ∈ L(G1 ) ; en a effet, nous pouvons exhiber la suite de d´rivations en une ´tape : e e 20

expression ⇒ expression "+" terme ⇒ terme "+" terme ⇒ terme "*" facteur "+" terme ⇒ facteur "*" facteur "+" terme ⇒ nombre "*" facteur "+" terme ⇒ nombre "*" identificateur "+" terme ⇒ nombre "*" identificateur "+" facteur ⇒ nombre "*" identificateur "+" nombre ´ Derivation gauche. La d´rivation pr´c´dente est appel´e une d´rivation gauche car elle est enti`rement e e e e e e compos´e de d´rivations en une ´tape dans lesquelles ` chaque fois c’est le non-terminal le plus ` gauche qui est e e e a a r´´crit. On peut d´finir de mˆme une d´rivation droite, o` ` chaque ´tape c’est le non-terminal le plus ` droite ee e e e ua e a qui est r´´crit. ee ´ Arbre de derivation. Soit w une chaˆ de symboles terminaux du langage L(G) ; il existe donc une ıne ∗ d´rivation telle que S0 ⇒ w. Cette d´rivation peut ˆtre repr´sent´e graphiquement par un arbre, appel´ arbre e e e e e e de d´rivation, d´fini de la mani`re suivante : e e e

expression expression terme terme facteur nombre 60 "*" * identificateur vitesse
Fig. 5 – Arbre de d´rivation e – la racine de l’arbre est le symbole de d´part, e – les nœuds int´rieurs sont ´tiquet´s par des symboles non terminaux, e e e – si un nœud int´rieur e est ´tiquet´ par le symbole S et si la production S → S1 S2 . . . Sk a ´t´ utilis´e pour e e e ee e d´river S alors les fils de e sont des nœuds ´tiquet´s, de la gauche vers la droite, par S1 , S2 . . . Sk , e e e – les feuilles sont ´tiquet´es par des symboles terminaux et, si on allonge verticalement les branches de e e l’arbre (sans les croiser) de telle mani`re que les feuilles soient toutes ` la mˆme hauteur, alors, lues de la e a e gauche vers la droite, elles constituent la chaˆ w. ıne Par exemple, la figure 5 montre l’arbre de d´rivation repr´sentant la d´rivation donn´e en exemple ci-dessus. e e e e On notera que l’ordre des d´rivations (gauche, droite) ne se voit pas sur l’arbre. e 3.1.3 Qualit´s des grammaires en vue des analyseurs e

terme facteur facteur

"+" +

nombre 200

Etant donn´e une grammaire G = {VT , VN , S0 , P }, faire l’analyse syntaxique d’une chaˆ w ∈ VT ∗ c’est e ıne r´pondre ` la question  w appartient-elle au langage L(G) ? . Parlant strictement, un analyseur syntaxique est e a donc un programme qui n’extrait aucune information de la chaˆ analys´e, il ne fait qu’accepter (par d´faut) ıne e e ou rejeter (en annon¸ant une erreurs de syntaxe) cette chaˆ c ıne. En r´alit´ on ne peut pas empˆcher les analyseurs d’en faire un peu plus car, pour prouver que w ∈ L(G) e e e ∗ il faut exhiber une d´rivation S0 ⇒ w, c’est-`-dire construire un arbre de d´rivation dont la liste des feuilles e a e est w. Or, cet arbre de derivation est d´j` une premi`re information extraite de la chaˆ source, un d´but de ea e ıne e  compr´hension  de ce que le texte signifie. e 21

Nous examinons ici des qualit´s qu’une grammaire doit avoir et des d´fauts dont elle doit ˆtre exempte pour e e e que la construction de l’arbre de d´rivation de toute chaˆ du langage soit possible et utile. e ıne ¨ Grammaires ambigues. Une grammaire est ambigu¨ s’il existe plusieurs d´rivations gauches diff´rentes e e e pour une mˆme chaˆ de terminaux. Par exemple, la grammaire G2 suivante est ambigu¨ : e ıne e expression → expression "+" expression | expression "*" expression | facteur facteur → nombre | identificateur | "(" expression ")" (G2 )

En effet, la figure 6 montre deux arbres de d´rivation distincts pour la chaˆ "2 * 3 + 10". Ils correspondent e ıne aux deux d´rivations gauches distinctes : e 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

2

nombre nombre * 3 + 10

Fig. 6 – Deux arbres de d´rivation pour la mˆme chaˆ e e ıne Deux grammaires sont dites ´quivalentes si elles engendrent le mˆme langage. Il est souvent possible de e e remplacer une grammaire ambigu¨ par une grammaire non ambigu¨ ´quivalente, mais il n’y a pas une m´thode e ee e g´n´rale pour cela. Par exemple, la grammaire G1 est non ambigu¨ et ´quivalente ` la grammaire G2 ci-dessus. e e e e a ´ ` Grammaires recursives a gauche. Une grammaire est r´cursive ` gauche s’il existe un non-terminal A e a ∗ et une d´rivation de la forme A ⇒ Aα, o` α est une chaˆ quelconque. Cas particulier, on dit qu’on a une e u ıne r´cursivit´ ` gauche simple si la grammaire poss`de une production de la forme A → Aα. e ea e La r´cursivit´ ` gauche ne rend pas une grammaire ambigu¨, mais empˆche l’´criture d’analyseurs pour cette e ea e e e grammaire, du moins des analyseurs descendants24 . Par exemple, la grammaire G1 de la section 3.1.1 est r´cursive ` gauche, et mˆme simplement : e a e expression → expression "+" terme | terme terme → terme "*" facteur | facteur facteur → nombre | identificateur | "(" expression ")" (G1 )

Il existe une m´thode pour obtenir une grammaire non r´cursive ` gauche ´quivalente ` une grammaire e e a e a donn´e. Dans le cas de la r´cursivit´ ` gauche simple, cela consiste ` remplacer une production telle que e e ea a
24 La question est un peu pr´matur´e ici, mais nous verrons qu’un analyseur descendant est un programme compos´ de fonctions e e e directement d´duites des productions. Une production A → . . . donne lieu ` une fonction reconnaitre A dont le corps est fait e a d’appels aux fonctions reconnaissant les symboles du membre droit de la production. Dans le cas d’une production A → Aα on se retrouve donc avec une fonction reconnaitre A qui commence par un appel de reconnaitre A. Bonjour la r´cursion infinie... e

22

A → Aα | β par les deux productions25 A → βA A → αA | ε En appliquant ce proc´d´ ` la grammaire G1 on obtient la grammaire G3 suivante : e ea expression → terme fin expression fin expression → "+" terme fin expression | ε terme → facteur fin terme fin terme → "*" facteur fin terme | ε facteur → nombre | identificateur | "(" expression ")"

(G3 )

A propos des ε-productions. La transformation de grammaire montr´e ci-dessus a introduit des produce tions avec un membre droit vide, ou ε-productions. Si on ne prend pas de disposition particuli`re, on aura un e probl`me pour l’´criture d’un analyseur, puisqu’une production telle que e e fin expression → "+" terme fin expression | ε impliquera notamment que  une mani`re de reconnaˆ une fin expression consiste ` ne rien reconnaˆ , ce e ıtre a ıtre qui est possible quelle que soit la chaˆ d’entr´e ; ainsi, notre grammaire semble devenir ambigu¨. On r´sout ce ıne e e e probl`me en imposant aux analyseurs que nous ´crirons la r`gle de comportement suivante : dans la d´rivation e e e e d’un non-terminal, une ε-production ne peut ˆtre choisie que lorsqu’aucune autre production n’est applicable. e Dans l’exemple pr´c´dent, cela donne : si la chaˆ d’entr´e commence par + alors on doit n´cessairement e e ıne e e choisir la premi`re production. e ` Factorisation a gauche. Nous cherchons ` ´crire des analyseurs pr´dictifs. Cela veut dire qu’` tout a e e a moment le choix entre productions qui ont le mˆme membre gauche doit pouvoir se faire, sans risque d’erreur, e en comparant le symbole courant de la chaˆ ` analyser avec les symboles susceptibles de commencer les ıne a d´rivations des membres droits des productions en comp´tition. e e Une grammaire contenant des productions comme A → αβ1 | αβ2 viole ce principe car lorsqu’il faut choisir entre les productions A → αβ1 et A → αβ2 le symbole courant est un de ceux qui peuvent commencer une d´rivation de α, et on ne peut pas choisir ` coup sˆr entre αβ1 et αβ2 . e a u Une transformation simple, appel´e factorisation ` gauche, corrige ce d´faut (si les symboles susceptibles de e a e commencer une r´´criture de β1 sont distincts de ceux pouvant commencer une r´´criture de β2 ) : ee ee A → αA A → β1 | β2 Exemple classique. Les grammaires de la plupart des langages de programmation d´finissent ainsi l’instruce tion conditionnelle : instr si → si expr alors instr | si expr alors instr sinon instr Pour avoir un analyseur pr´dictif il faudra op´rer une factorisation ` gauche : e e a instr si → si expr alors instr fin instr si fin instr si → sinon instr | ε Comme pr´c´demment, l’apparition d’une ε-production semble rendre ambigu¨ la grammaire. Plus pr´cis´ment, e e e e e la question suivante se pose : n’y a-t-il pas deux arbres de d´rivation possibles pour la chaˆ 26 : e ıne si α alors si β alors γ sinon δ Nous l’avons d´j` dit, on l`ve cette ambigu¨ e en imposant que la ε-production ne peut ˆtre choisie que ea e ıt´ e si aucune autre production n’est applicable. Autrement dit, si, au moment o` l’analyseur doit d´river le nonu e ıne e terminal fin instr si, la chaˆ d’entr´e commence par le terminal sinon, alors la production  fin instr si → sinon instr  doit ˆtre appliqu´e. La figure 7 montre l’arbre de d´rivation obtenu pour la chaˆ pr´c´dente. e e e ıne e e
25 Pour se convaincre de l’´quivalence de ces deux grammaires il suffit de s’apercevoir que, si α et β sont des symboles terminaux, e alors elles engendrent toutes deux le langage {β, βα, βαα, βααα, . . .} 26 Autre formulation de la mˆme question :  sinon  se rattache-t-il ` la premi`re ou ` la deuxi`me instruction  si  ? e a e a e

23

instr_si si expr α si alors expr β alors instr instr γ fin_instr_si fin_instr_si sinon instr δ
Fig. 7 – Arbre de d´rivation pour la chaˆ si α alors si β alors γ sinon δ e ıne

ε

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´guli`res, mais il existe des langages (pratiquement tous les langages de programmation, excusez du peu... !) e e qu’elles ne peuvent pas d´crire compl`tement. e e ∗ On d´montre par exemple que le langage L = wcw | w ∈ (a|b) , o` a, b et c sont des terminaux, ne peut e u pas ˆtre d´crit par une grammaire non contextuelle. L est fait de phrases comportant deux chaˆ e e ınes de a et b identiques, s´par´es par un c, comme ababcabab. L’importance de cet exemple provient du fait que L mod´lise e e e l’obligation, qu’ont la plupart des langages, de v´rifier que les identificateurs apparaissant dans les instructions e ont bien ´t´ pr´alablement d´clar´s (la premi`re occurrence de w dans wcw correspond ` la d´claration d’un ee e e e e a e identificateur, la deuxi`me occurrence de w ` l’utilisation de ce dernier). e a Autrement dit, l’analyse syntaxique ne permet pas de v´rifier que les identificateurs utilis´s dans les proe e grammes font l’objet de d´clarations pr´alables. Ce probl`me doit n´cessairement ˆtre remis ` une phase e e e e e a ult´rieure d’analyse s´mantique. e e

3.2

Analyseurs descendants

Etant donn´e une grammaire G = (VT , VN , S0 , P ), analyser une chaˆ de symboles terminaux w ∈ VT ∗ c’est e ıne ∗ construire un arbre de d´rivation prouvant que S0 ⇒ w. e Les grammaires des langages que nous cherchons ` analyser ont un ensemble de propri´t´s qu’on r´sume en a ee e disant que ce sont des grammaires LL(1). Cela signifie qu’on peut en ´crire des analyseurs : e – lisant la chaˆ source de la gauche vers la droite (gauche = left, c’est le premier L), ıne – cherchant ` construire une d´rivation gauche (c’est le deuxi`me L), a e e – dans lesquels un seul symbole de la chaˆ source est accessible ` chaque instant et permet de choisir, ıne a lorsque c’est n´cessaire, une production parmi plusieurs candidates (c’est le 1 de LL(1)). e A propos du symbole accesible. Pour r´fl´chir au fonctionnement de nos analyseurs il est utile d’imaginer e e que la chaˆ source est ´crite sur un ruban d´filant derri`re une fenˆtre, de telle mani`re qu’un seul symbole ıne e e e e e est visible ` la fois ; voyez la figure 8. Un m´canisme permet de faire avancer –jamais reculer– le ruban, pour a e rendre visible le symbole suivant.

if
chaîne source déjà examinée
unité courante

avancer

chaîne source restant à examiner

Fig. 8 – Fenˆtre ` symboles terminaux e a

24

Lorsque nous programmerons effectivement des analyseurs, cette  machine ` symboles terminaux  ne sera a rien d’autre que l’analyseur lexical pr´alablement ´crit ; le symbole visible ` la fenˆtre sera repr´sent´ par une e e a e e e variable uniteCourante, et l’op´ration  faire avancer le ruban  se traduira par uniteCourante = uniteSuivante() e (ou bien, si c’est lex qui a ´crit l’analyseur lexical, uniteCourante = yylex()). e 3.2.1 Principe

Analyseur descendant. Un analyseur descendant construit l’arbre de d´rivation de la racine (le symbole e de d´part de la grammaire) vers les feuilles (la chaˆ de terminaux). e ıne Pour en d´crire sch´matiquement le fonctionnement nous nous donnons une fenˆtre ` symboles terminaux e e e a comme ci-dessus et une pile de symboles, c’est-`-dire une s´quence de symboles terminaux et non terminaux a e a ` laquelle on ajoute et on enl`ve des symboles par une mˆme extr´mit´, en l’occurrence l’extr´mit´ de gauche e e e e e e (c’est une pile couch´e ` l’horizontale, qui se remplit de la droite vers la gauche). e a Initialisation. Au d´part, la pile contient le symbole de d´part de la grammaire et la fenˆtre montre le e e e premier symbole terminal de la chaˆ d’entr´e. ıne e ´ Iteration. Tant que la pile n’est pas vide, r´p´ter les op´rations suivantes e e e – si le symbole au sommet de la pile (c.-`-d. le plus ` gauche) est un terminal α a a – si le terminal visible ` la fenˆtre est le mˆme symbole α, alors a e e – d´piler le symbole au sommet de la pile et e – faire avancer le terminal visible ` la fenˆtre, a e – sinon, signaler une erreur (par exemple afficher  α attendu ) ; – si le symbole au sommet de la pile est un non terminal S e – s’il y a une seule production S → S1 S2 . . . Sk ayant S pour membre gauche alors d´piler S et empiler S1 S2 . . . Sk ` la place, a – s’il y a plusieurs productions ayant S pour membre gauche, alors d’apr`s le terminal e visible ` la fenˆtre, sans faire avancer ce dernier, choisir l’unique production S → a e S1 S2 . . . Sk pouvant convenir, d´piler S et empiler S1 S2 . . . Sk . e Terminaison. Lorsque la pile est vide – si le terminal visible ` la fenˆtre est la marque qui indique la fin de la chaˆ d’entr´e alors l’analyse a e ıne e a r´ussi : la chaˆ appartient au langage engendr´ par la grammaire, e ıne e – sinon, signaler une erreur (par exemple, afficher  caract`res inattendus ` la suite d’un texte e a correct ). A titre d’exemple, voici la reconnaissance par un tel analyseur du texte "60 * vitesse + 200" avec la grammaire G3 de la section 3.1.3 : expression → terme fin expression fin expression → "+" terme fin expression | ε terme → facteur fin terme fin terme → "*" facteur fin terme | ε facteur → nombre | identificateur | "(" expression ")"

(G3 )

La chaˆ d’entr´e est donc (nombre "*" identif "+" nombre). Les ´tats successifs de la pile et de la ıne e e fenˆtre sont les suivants : e

25

fenˆtre e nombre nombre nombre nombre "*" "*" identificateur identificateur "+" "+" "+" "+" nombre nombre nombre ¶ ¶ ¶

pile terme terme terme terme terme terme terme terme ε fin fin fin fin fin fin fin fin fin fin fin fin fin fin fin expression expression expression expression expression expression expression expression expression expression expression expression expression expression expression expression ε

facteur fin nombre fin fin "*" facteur fin facteur fin identificateur fin fin

"+" terme terme facteur nombre

Lorsque la pile est vide, la fenˆtre exhibe ¶, la marque de fin de chaˆ e ıne. La chaˆ donn´e appartient donc ıne e bien au langage consid´r´. ee 3.2.2 Analyseur descendant non r´cursif e

Nous ne d´velopperons pas cette voie ici, mais nous pouvons remarquer qu’on peut r´aliser des programmes e e it´ratifs qui implantent l’algorithme expliqu´ ` la section pr´c´dente. e ea e e La plus grosse difficult´ est le choix d’une production chaque fois qu’il faut d´river un non terminal qui e e est le membre gauche de plusieurs productions de la grammaire. Comme ce choix ne d´pend que du terminal e visible ` la fenˆtre, on peut le faire, et de mani`re tr`s efficace, ` l’aide d’une table ` double entr´e, appel´e table a e e e a a e e d’analyse, calcul´e ` l’avance, repr´sentant une fonction Choix : VN × VT → P qui ` un couple (S, α) form´ e a e a e d’un non terminal (le sommet de la pile) et un terminal (le symbole visible ` la fenˆtre) associe la production a e qu’il faut utiliser pour d´river S. e Pour avoir un analyseur descendant non r´cursif il suffit alors de se donner une fenˆtre ` symboles terminaux e e a (c’est-`-dire un analyseur lexical), une pile de symboles comme expliqu´ ` la section pr´c´dente, une table a e a e e d’analyse comme expliqu´ ici et un petit programme qui implante l’algorithme de la section pr´c´dente, dans e e e lequel la partie  choisir la production...  se r´sume ` une consultation de la table P = Choix(S, α). e a En d´finitive, un analyseur descendant est donc un couple form´ d’une table dont les valeurs sont intimement e e li´es ` la grammaire analys´e et d’un programme tout ` fait ind´pendant de cette grammaire. e a e a e 3.2.3 Analyse par descente r´cursive e

A l’oppos´ du pr´c´dent, un analyseur par descente r´cursive est un type d’analyseur descendant dans lequel e e e e le programme de l’analyseur est ´troitement li´ ` la grammaire analys´e. Voici les principes de l’´criture d’un e ea e e tel analyseur : e a 1. Chaque groupe de productions ayant le mˆme membre gauche S donne lieu ` une fonction void reconnaitre S(void), ou plus simplement void S(void). Le corps de cette fonction se d´duit des membres droits e des productions en question, comme expliqu´ ci-apr`s. e e e 2. Lorsque plusieurs productions ont le mˆme membre gauche, le corps de la fonction correspondante est une conditionnelle (instruction if ) ou un aiguillage (instruction switch) qui, d’apr`s le symbole terminal e visible ` la fenˆtre, s´lectionne l’ex´cution des actions correspondant au membre droit de la production a e e e pertinente. Dans cette s´lection, le symbole visible ` la fenˆtre n’est pas modifi´. e a e e 3. Une s´quence de symboles S1 S2 . . . Sn dans le membre droit d’une production donne lieu, dans la fonction e correspondante, ` une s´quence d’instructions traduisant les actions  reconnaissance de S1 ,  recona e naissance de S2 , . . . reconnaissance de Sn . 26

4. Si S est un symbole non terminal, l’action  reconnaissance de S  se r´duit ` l’appel de fonction recone a naitre S(). 5. Si α est un symbole terminal, l’action  reconnaissance de α  consiste ` consid´rer le symbole terminal a e visible ` la fenˆtre et a e – s’il est ´gal ` α, faire passer la fenˆtre sur le symbole suivant27 , e a e – sinon, annoncer une erreur (par exemple, afficher  α attendu ). L’ensemble des fonctions ´crites selon les prescriptions pr´c´dentes forme l’analyseur du langage consid´r´. e e e ee L’initialisation de l’analyseur consiste ` positionner la fenˆtre sur le premier terminal de la chaˆ d’entr´e. a e ıne e On lance l’analyse en appellant la fonction associ´e au symbole de d´part de la grammaire. Au retour de cette e e fonction – si la fenˆtre ` terminaux montre la marque de fin de chaˆ l’analyse a r´ussi, e a ıne, e – sinon la chaˆ est erron´e28 (on peut par exemple afficher le message  caract`res ill´gaux apr`s une ıne e e e e expression correcte ). Exemple. Voici encore la grammaire G3 de la section 3.1.3 : expression → terme fin expression fin expression → "+" terme fin expression | ε terme → facteur fin terme fin terme → "*" facteur fin terme | ε facteur → nombre | identificateur | "(" expression ")" et voici l’analyseur par descente r´cursive correspondant : e 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 c’est ici le seul endroit, dans cette description, o` il est indiqu´ de faire avancer la fenˆtre. u e e autre attitude possible -on l’a vue adopt´e par certains compilateurs de Pascal- consiste ` ne rien v´rifier au retour de e a e la fonction associ´e au symbole de d´part. Cela revient ` consid´rer que, si le texte source comporte un programme correct, peu e e a e importent les ´ventuels caract`res qui pourraient suivre. e e
28 Une 27 Notez

(G3 )

27

else if (uniteCourante == IDENTIFICATEUR) terminal(IDENTIFICATEUR); else { terminal(’(’); expression(); terminal(’)’); } } La reconnaissance d’un terminal revient fr´quemment dans un analyseur. Nous en avons fait une fonction e s´par´e (on suppose que erreur est une fonction qui affiche un message et termine le programme) : e e 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 ´crit le programme pr´c´dent en appliquant syst´matiquement les r`gles donn´es plus e e e e e e haut, obtenant ainsi un analyseur correct dont la structure refl`te la grammaire donn´e. Mais il n’est pas e e interdit de pratiquer ensuite certaines simplifications, ne serait-ce pour rattraper certaines maladresses de notre d´marche. L’appel g´n´ralis´ de la fonction terminal, notamment, est ` l’origine de certains test redondants. e e e e a Par exemple, la fonction fin expression commence par les deux lignes if (uniteCourante == ’+’) { terminal(’+’); ... si on d´veloppe l’appel de terminal, la maladresse de la chose devient ´vidente e e if (uniteCourante == ’+’) { if (uniteCourante == ’+’) lireUnite(); ... Une version plus raisonnable des fonctions fin 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 ´crite ci-dessus, la fonction facteur aura tendance ` faire passer toutes les erreurs par le e a diagnostic  ’(’ attendu , ce qui risque de manquer d’` propos. Une version encore plus raisonnable de cette a 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 l’avons dit, ´tant donn´es une grammaire G = {VT , VN , S0 , P } et une chaˆ w ∈ VT ∗ , le e e ıne but de l’analyse syntaxique est la construction d’un arbre de d´rivation qui prouve w ∈ L(G). Les m´thodes e e descendantes construisent cet arbre en partant du symbole de d´part de la grammaire et en  allant vers  la e chaˆ de terminaux. Les m´thodes ascendantes, au contraire, partent des terminaux qui constituent la chaˆ ıne e ıne d’entr´e et  vont vers  le symbole de d´part. e e Le principe g´n´ral des m´thodes ascendantes est de maintenir une pile de symboles29 dans laquelle sont e e e empil´s (l’empilement s’appelle ici d´calage) les terminaux au fur et ` mesure qu’ils sont lus, tant que les e e a symboles au sommet de la pile ne sont pas le membre droit d’une production de la grammaire. Si les k symboles du sommet de la pile constituent le membre droit d’une production alors ils peuvent ˆtre d´pil´s et remplac´s e e e e par le membre gauche de cette production (cette op´ration s’appelle r´duction). Lorsque dans la pile il n’y a e e plus que le symbole de d´part de la grammaire, si tous les symboles de la chaˆ d’entr´e ont ´t´ lus, l’analyse e ıne e ee a r´ussi. e Le probl`me majeur de ces m´thodes est de faire deux sortes de choix : e e – si les symboles au sommet de la pile constituent le membre droit de deux productions distinctes, laquelle utiliser pour effectuer la r´duction ? e – lorsque les symboles au sommet de la pile sont le membre droit d’une ou plusieurs productions, faut-il r´duire tout de suite, ou bien continuer ` d´caler, afin de permettre ult´rieurement une r´duction plus e a e e e juste ? A titre d’exemple, 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 d’entr´e "60 * vitesse + 200", c’est-`-dire la e a chaˆ de terminaux (nombre "*" identificateur "+" nombre) : ıne
29 Comme pr´c´demment, cette pile est un tableau couch´ ` l’horizontale ; mais cette fois elle grandit de la gauche vers la droite, e e ea c’est-`-dire que son sommet est son extr´mit´ droite. a e e

29

fenˆtre e nombre "*" "*" "*" identificateur "+" "+" "+" "+" nombre ¶ ¶ ¶ ¶

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

identificateur facteur

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

action d´calage e r´duction e r´duction e d´calage e d´calage e r´duction e r´duction e r´duction e d´calage e d´calage e r´duction e r´duction e r´duction e succ`s e

On dit que les m´thodes de ce type effectuent une analyse par d´calage-r´duction. Comme le montre le e e e tableau ci-dessus, le point important est le choix entre r´duction et d´calage, chaque fois qu’une r´duction est e e e possible. Le principe est : les r´ductions pratiqu´es r´alisent la construction inverse d’une d´rivation droite. e e e e Par exemple, le r´ductions faites dans l’analyse pr´c´dente construisent, ` l’envers, la d´rivation droite e e e a e 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´ les apparences, de construire des analyseurs ascendants plus efficaces que les analyseurs e descendants, et acceptant une classe de langages plus large que la classe des langages trait´s par ces derniers. e Le principal inconv´nient de ces analyseurs est qu’ils n´cessitent des tables qu’il est extrˆmement difficile e e e de construire ` la main. Heureusement, des outils existent pour les construire automatiquement, ` partir de la a a grammaire du langage ; la section 3.4 pr´sente yacc, un des plus connus de ces outils. e Les analyseurs LR(k) lisent la chaˆ d’entr´e de la gauche vers la droite (d’o` le L), en construisant ıne e u l’inverse d’une d´rivation droite (d’o` le R) avec une vue sur la chaˆ d’entr´e large de k symboles ; lorsqu’on e u ıne e dit simplement LR on sous-entend k = 1, c’est le cas le plus fr´quent. e Etant donn´e une grammaire G = (VT , VN , S0 , P ), un analyseur LR est constitu´ par la donn´e d’un ensemble e e e d’´tats E, d’une fenˆtre ` symboles terminaux (c’est-`-dire un analyseur lexical), d’une pile de doublets (s, e) e e a a o` s ∈ E et e ∈ VT et de deux tables Action et Suivant, qui repr´sentent des fonctions : u e Action : E × VT → ({ decaler } × E) ∪ ({reduire} × P ) ∪ { succes, erreur } Suivant : E × VN → E Un analyseur LR comporte enfin un programme, ind´pendant du langage analys´, qui ex´cute les op´rations e e e e suivantes : Initialisation. Placer la fenˆtre sur le premier symbole de la chaˆ d’entr´e et vider la pile. e ıne e It´ration. Tant que c’est possible, r´p´ter : e e e soit s l’´tat au sommet de la pile et α le terminal visible ` la fenˆtre e a e si Action(s, α) = (decaler, s ) empiler (α, s ) placer la fenˆtre sur le prochain symbole de la chaˆ d’entree e ıne

30

sinon, si Action(s, α) = (reduire, A → β) d´piler autant d’´l´ments de la pile qu’il y a de symboles dans β e ee (soit (γ, s ) le nouveau sommet de la pile) empiler (A, Suivant(s , A)) sinon, si Action(s, α) = succes arrˆt e sinon erreur. Note. En r´alit´, notre pile est redondante. Les ´tats de l’analyseur repr´sentent les diverses configurations e e e e dans lesquelles la pile peut se trouver, il n’y a donc pas besoin d’empiler les symboles, les ´tats suffisent. Nous e avons utilis´ une pile de couples (etat, symbole) pour clarifier l’explication. e

3.4

Yacc, un g´n´rateur d’analyseurs syntaxiques e e

Comme nous l’avons indiqu´, les tables Action et Suivant d’un analyseur LR sont difficiles ` construire e a manuellement, mais il existe des outils pour les d´duire automatiquement des productions de la grammaire e consid´r´e. ee Le programme yacc 30 est un tel g´n´rateur d’analyseurs syntaxiques. Il prend en entr´e un fichier source e e e constitu´ essentiellement des productions d’une grammaure non contextuelle G et sort ` titre de r´sultat un e a e programme C qui, une fois compil´, est un analyseur syntaxique pour le langage L(G). e

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

y.tab.h yacc y.tab.c

gcc

monProg

monProg

résultat de l'analyse

Fig. 9 – Utilisation courante de yacc Dans la description de la grammaire donn´e ` yacc on peut associer des actions s´mantiques aux productions ; e a e ce sont des bouts de code source C que yacc place aux bons endroits de l’analyseur construit. Ce dernier peut ainsi ex´cuter des actions ou produire des informations d´duites du texte source, c’est-`-dire devenir un compilateur. e e a Un analyseur syntaxique requiert pour travailler un analyseur lexical qui lui d´livre le flot d’entr´e sous forme e e d’unit´s lexicales successives. Par d´faut, yacc suppose que l’analyseur lexical disponible a ´t´ fabriqu´ par lex. e e ee e Autrement dit, sans qu’il faille de d´claration sp´ciale pour cela, le programme produit par yacc comporte des e e appels de la fonction yylex aux endroits o` l’acquisition d’une unite lexicale est n´cessaire. u e 3.4.1 Structure d’un fichier source pour yacc

Un fichier source pour yacc doit avoir un nom termin´ par  .y . Il est fait de trois sections, d´limit´es par e e e deux lignes r´duites au symbole %% : e %{ d´clarations pour le compilateur C e %}
30 Dans le monde Linux on trouve une version am´lior´e de yacc, nomm´e bison, qui appartient ` la famille GNU. Le nom de e e e a 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´clarations pour yacc e %% r`gles (productions + actions s´mantiques) e e %% fonctions C suppl´mentaires e Les parties  d´clarations pour le compilateur C  et  fonctions C suppl´mentaires  sont simplement e e recopi´es dans le fichier produit, respectivement au d´but et ` la fin de ce fichier. Chacune de ces deux parties e e a peut ˆtre absente. e Dans la partie  d´clarations pour yacc  on rencontre souvent les d´clarations des unit´s lexicales, sous une e e e forme qui laisse yacc se charger d’inventer 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´clarations d’unit´s lexicales int´ressent yacc, qui les utilise, mais aussi lex, qui les manipule en tant que e e e r´sultats de la fonction yylex. Pour cette raison, yacc produit31 un fichier suppl´mentaire, nomm´ y.tab.h32 , e e e destin´ ` ˆtre inclus dans le source lex (au lieu du fichier que nous avons appel´ unitesLexicales.h dans e a e e l’exemple de la section 2.3.2). Par exemple, le fichier produit pour les d´clarations ci-dessus ressemble ` ceci : e a #define #define #define ... #define #define NOMBRE IDENTIF VARIABLE FAIRE RETOUR 257 258 259 272 273

Notez que yacc consid`re que tout caract`re est susceptible de jouer le rˆle d’unit´ lexicale (comme cela a e e o e ´t´ le cas dans notre exemple de la section 2.3.2) ; pour cette raison, ces constantes sont num´rot´es ` partir de ee e e a 256. ´ Specification de VT , VN et S0 . Dans un fichier source yacc : – les caract`res simples, encadr´s par des apostrophes comme dans les programmes C, et les identificateurs e e mentionn´s dans les d´clarations %token sont tenus pour des symboles terminaux, e e – tous les autres identificateurs apparaissant dans les productions sont consid`res comme des symboles non e terminaux, – par d´faut, le symbole de d´part est le membre gauche de la premi`re r`gle. e e e e ` Regles de traduction. Une r`gle de traduction est un ensemble de productions ayant le mˆme membre e e gauche, chacune associ´ ` une action s´mantique. ea e Une action s´mantique (cf. section 3.4.2) est un morceau de code source C encadr´ par des accolades. C’est e e un code que l’analyseur ex´cutera lorsque la production correspondante aura ´t´ utilis´e dans une r´duction. Si e ee e e on ´crit un analyseur  pur , c’est-`-dire un analyseur qui ne fait qu’accepter ou rejeter la chaˆ d’entr´e, alors e a ıne e il n’y a pas d’actions s´mantiques et les r`gles de traduction sont simplement les productions de la grammaire. e e Dans les r`gles de traduction, le m´ta-symbole → est indiqu´ par deux points  :  et chaque r`gle (c’este e e e a `-dire chaque groupe de productions avec le mˆme membre gauche) est termin´e par un point-virgule  ; . La e e barre verticale  |  a la mˆme signification que dans la notation des grammaires. e 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 ´crite dans un fichier source yacc (pour obtenir un analyseur pur, sans actions s´mantiques) : e e %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 l’option -d, comme dans  yacc -d maSyntaxe.y . le cas de bison les noms des fichiers  .tab.c  et  .tab.h  refl`tent le nom du fichier source  .y . e

32

L’analyseur syntaxique se pr´sente comme une fonction int yyparse(void), qui rend 0 lorsque la chaˆ e ıne d’entr´e est accept´e, une valeur non nulle dans le cas contraire. Pour avoir un analyseur syntaxique autonome e e il suffit donc d’ajouter, en troisi`me section du fichier pr´c´dent : e e e %% int main(void) { if (yyparse() == 0) printf("Texte correct\n"); } En r´alit´, il faut aussi ´crire la fonction appel´e en cas d’erreur. C’est une fonction de prototype void e e e e yyerror(char *message), elle est appel´e par l’analyseur avec un message d’erreur (par d´faut  parse error , e e ce n’est pas tr`s informatif !). Par exemple : e void yyerror(char *message) { printf(" <<< %s\n", message); } N.B. L’effet pr´cis de l’instruction ci-dessus, c’est-`-dire la pr´sentation effective des messages d’erreur, e a e d´pend de la mani`re dont l’analyseur lexical ´crit les unit´s lexicales au fur et ` mesure de l’analyse. e e e e a 3.4.2 Actions s´mantiques et valeurs des attributs e

Une action s´mantique est une s´quence d’instructions C ´crite, entre accolades, ` droite d’une production. e e e a Cette s´quence est recopi´e par yacc dans l’analyseur produit, de telle mani`re qu’elle sera ex´cut´e, pendant e e e e e l’analyse, lorsque la production correspondante aura ´t´ employ´e pour faire une r´duction (cf.  analyse par ee e e d´calage-r´duction  ` la section 3.3). e e a Voici un exemple simple, mais complet. Le programme suivant lit une expression arithm´tique infix´e33 e e form´e de nombres, d’identificateurs et des op´rateurs + et ∗, et ´crit la repr´sentation en notation postfix´e34 e e e e e de la mˆme expression. e Fichier lexique.l : %{ #include "syntaxe.tab.h" extern char nom[]; /* cha^ne de caract`res partag´e avec l’analyseur syntaxique */ ı e e %} 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`res partag´e avec l’analyseur lexical */ ı e e { { { { } yylval = atoi(yytext); return nombre; } strcpy(nom, yytext); return identificateur; } return yytext[0]; }

33 Dans la notation infix´e, un op´rateur binaire est ´crit entre ses deux op´randes. C’est la notation habituelle, et elle est ambigu¨ ; e e e e e c’est pourquoi on lui associe un syst`me d’associativit´ et de priorit´ des op´rateurs, et la possibilit´ d’utiliser des parenth`ses. e e e e e e 34 Dans la notation postifix´e, appel´e aussi notation polonaise inverse, un op´rateur binaire est ´crit ` la suite de ses deux e e e e a op´randes ; cette notation n’a besoin ni de priorit´s des op´rateurs ni de parenth`ses, et elle n’est pas ambigu¨. e e e e 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 ` la a caract´risation du symbole. Par exemple, dans les langages qui nous int´ressent, la reconnaissance du lex`me e e e "2001" donne lieu ` l’unit´ lexicale NOMBRE avec l’attribut 2001. a e Un analyseur lexical produit par lex transmet les attributs des unit´s lexicales ` un analyseur syntaxique e a produit par yacc ` travers une variable yylval qui, par d´faut35 , est de type int. Si vous allez voir le fichier a e  .tab.h  fabriqu´ par yacc et destin´ ` ˆtre inclus dans l’analyseur lexical, vous y trouverez, outre les d´finitions e eae e des codes des unit´s lexicales, les d´clarations : e e #define YYSTYPE int ... extern YYSTYPE yylval; Nous avons dit que les actions s´mantiques sont des bouts de code C que yacc se borne ` recopier dans e a l’analyseur produit. Ce n’est pas tout ` fait exact, dans les actions s´mantiques on peut mettre certains symboles a e bizarres, que yacc remplace par des expressions C correctes. Ainsi, $1, $2, $3, etc. d´signent les valeurs des e attributs des symboles constituant le membre droit de la production concern´e, tandis que $$ d´signe la valeur e e de l’attribut du symbole qui est le membre gauche de cette production.
35 Un m´canisme puissant et relativement simple permet d’avoir des attributs polymorphes, pouvant prendre plusieurs types e distincts. Nous ne l’´tudierons pas dans le cadre de ce cours. e

34

L’action s´mantique { $$ = $1 ; } est implicite et il n’y a pas besoin de l’´crire. e e Par exemple, voici notre analyseur pr´c´dent, modifi´ (l´g`rement) pour en faire un calculateur de bureau e e e e e effectuant les quatre op´rations ´l´mentaires sur des nombres entiers, avec gestion des parenth`ses et de la e ee e priorit´ des op´rateurs : e e Fichier lexique.l : le mˆme que pr´c´demment, ` ceci pr`s que les identificateurs ne sont plus reconnus. e e e a e Fichier syntaxe.y : %{ void yyerror(char *); %} %token nombre %% session

expression

terme

facteur

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

session expression ’=’

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

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 d’utilisation ; ¶ repr´sente la touche  fin de fichier , qui d´pend du syst`me utilis´ (Ctrl-D, pour e e e e UNIX) : $ go 2 + 3 = r´sultat : 5 e (2 + 3)*(1002 - 1 - 1) = r´sultat : 5000 e ¶ Au revoir! 3.4.3 Conflits et ambigu¨ es ıt´

Voici encore une grammaire ´quivalente ` la pr´c´dente, mais plus compacte : e a e e ... %% session

: session expression ’=’ | ;

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

35

expression

: | | | | | ;

expression ’+’ expression ’-’ expression ’*’ expression ’/’ ’(’ expression nombre

expression expression expression expression ’)’

{ { { { {

$$ $$ $$ $$ $$

= = = = =

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

} } } }

%% ... Nous avons vu ` la section 3.1.3 que cette grammaire est ambigu¨ ; elle provoquera donc des conflits. Lorsqu’il a e rencontre un conflit36 , yacc applique une r`gle de r´solution par d´faut et continue son travail ; ` la fin de ce e e e a dernier, il indique le nombre total de conflits rencontr´s et arbitrairement r´solus. Il est imp´ratif de comprendre e e e la cause de ces conflits et il est fortement recommand´ d’essayer de les supprimer (par exemple en transformant e la grammaire). Les conflits possibles sont : 1. D´caler ou r´duire ? ( shift/reduce conflict ). Ce conflit se produit lorsque l’algorithme de yacc n’arrive e e pas ` choisir entre d´caler et r´duire, car les deux actions sont possibles et n’am`nent pas l’analyse ` une a e e e a impasse. Un exemple typique de ce conflit a pour origine la grammaire usuelle de l’instruction conditionnelle instr si → si expr alors instr | si expr alors instr sinon instr Le conflit apparaˆ pendant l’analyse d’une chaˆ comme ıt ıne 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´duire ces symboles en instr si (ce qui revient ` associer la partie sinon δ au premier si) ou bien faut-il e a d´caler (ce qui provoquera plus tard une r´duction revenant ` associer la partie sinon δ au second si) ? e e a R´solution par d´faut : l’analyseur fait le d´calage (c’est un comportement  glouton  : chacun cherche ` e e e a manger le plus de terminaux possibles). 2. Comment r´duire ? ( reduce/reduce conflict ) Ce conflit se produit lorsque l’algorithme ne peut pas e choisir entre deux productions distinctes dont les membres droits permettent tous deux de r´duire les symboles e au sommet de la pile. On trouve un exemple typique d’un tel conflit dans les grammaires de langages (il y en a !) o` on note avec u des parenth`ses aussi bien les appels de fonctions que les acc`s aux tableaux. Sans rentrer dans les d´tails, il e e e est facile d’imaginer qu’on trouvera dans de telles grammaires des productions compl`tement diff´rentes avec e e les mˆmes membres droits. Par exemple, la production d´finissant un appel de fonction et celle d´finissant un e e e acc`s ` un tableau pourraient ressembler ` ceci : e a a ... appel de fonction → identificateur ’(’ liste expressions ’)’ ... acces tableau → identificateur ’(’ liste expressions ’)’ ... La r´solution par d´faut est : dans l’ordre o` les r`gles sont ´crites dans le fichier source pour yacc, on pr´f`re e e u e e ee la premi`re production. Comme l’exemple ci-dessus le montre37 , cela ne r´sout pas souvent bien le probl`me. e e e ´ Grammaires d’operateurs. La grammaire pr´c´dente, ou du moins sa partie utile, la r`gle expression, e e e v´rifie ceci : e – aucune r`gle ne contient d’ε-production, e – aucune r`gle ne contient une production ayant deux non-terminaux cons´cutifs. e e
36 N’oubliez pas que yacc ne fait pas une analyse, mais un analyseur. Ce qu’il d´tecte en r´alit´ n’est pas un conflit, mais la e e e possibilit´ que l’analyseur produit en ait ult´rieurement, du moins sur certains textes sources. e e 37 Le probl`me consistant ` choisir assez tˆt entre appel de fonction et acc`s ` un tableau, lorsque les notations sont les mˆmes, e a o e a e est souvent r´solu par des consid´rations s´mantiques : l’identificateur qui pr´c`de la parenth`se est cens´ avoir ´t´ d´clar´, on e e e e e e e e e e e consulte donc la table de symboles pour savoir si c’est un nom de proc´dure ou bien un nom de tableau. Du point de vue de la e puissance des analyseurs syntaxiques, c’est donc plutˆt un aveu d’impuissance... o

36

De telles grammaires s’appellent des grammaires d’op´rateurs. Nous n’en ferons pas l’´tude d´taill´e ici, e e e e mais il se trouve qu’il est tr`s simple de les rendre non ambigu¨s : il suffit de pr´ciser par ailleurs le sens de e e e l’associativit´ et la priorit´ de chaque op´rateur. e e e En yacc, cela se fait par des d´clarations %left et %right qui sp´cifient le sens d’associativit´ des op´rateurs, e e e e l’ordre de ces d´clarations donnant leur priorit´ : ` chaque nouvelle d´claration les op´rateurs d´clar´s sont plus e e a e e e e prioritaires que les pr´c´dents. e e Ainsi, la grammaire pr´c´dente, pr´sent´e comme ci-apr`s, n’est plus ambigu¨. On a ajout´ des d´clarations e e e e e e e e indiquant que +, −, ∗ et / sont associatifs ` gauche38 et que la priorit´ de ∗ et / est sup´rieure ` celle de + et a e e a −. %{ void yyerror(char *); %} %token nombre %left ’+’ ’-’ %left ’*’ ’/’ %% session

expression

: | ; : | | | | | ;

session expression ’=’

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

expression ’+’ expression ’-’ expression ’*’ expression ’/’ ’(’ expression nombre

expression expression expression expression ’)’

{ { { { {

$$ $$ $$ $$ $$

= = = = =

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

} } } }

%% ...

4

Analyse s´mantique e

Apr`s l’analyse lexicale et l’analyse syntaxique, l’´tape suivante dans la conception d’un compilateur est e e l’analyse s´mantique dont la partie la plus visible est le contrˆle de type. Des exemples de tˆches li´es au e o a e contrˆle de type sont : o – construire et m´moriser des repr´sentations des types d´finis par l’utilisateur, lorsque le langage le permet, e e e – traiter les d´clarations de variables et fonctions et m´moriser les types qui leur sont appliqu´s, e e e – v´rifier que toute variable r´f´renc´e et toute fonction appel´e ont bien ´t´ pr´alablement d´clar´es, e ee e e ee e e e – v´rifier que les param`tres des fonctions ont les types requis, e e – contrˆler les types des op´randes des op´rations arithm´tiques et en d´duire le type du r´sultat, o e e e e e – au besoin, ins´rer dans les expressions les conversion impos´es par certaines r`gles de compatibilit´, e e e e – etc. Pour fixer les id´es, voici une situation typique o` le contrˆle de type joue. Imaginons qu’un programme, e u o ´crit en C, contient l’instruction  i = (200 + j) * 3.14 . L’analyseur syntaxique construit un arbre abstrait e repr´sentant cette expression, comme ceci (pour ˆtre tout ` fait corrects, ` la place de i et j nous aurions dˆ e e a a u repr´senter des renvois ` la table des symboles) : e a
38 Dire qu’un op´rateur ⊗ est associatif ` gauche [resp. ` droite] c’est dire que a ⊗ b ⊗ c se lit (a ⊗ b) ⊗ c [resp. a ⊗ (b ⊗ c)]. La e a a question est importante, mˆme pour des op´rateurs simples : on tient ` ce que 100 − 10 − 1 vaille 89 et non 91 ! e e a

37

= ¨ r ¨ r r ¨ i ∗ ¨rr ¨ + 3.14 ¨r ¨r 200 j Dans de tels arbres, seules les feuilles (ici i, 200, j et 3.14) ont des types pr´cis39 , tandis que les nœuds e internes repr´sentent des op´rations abstraites, dont le type exact reste ` pr´ciser. Le travail s´mantique ` faire e e a e e a consiste `  remonter les types , depuis les feuilles vers la racine, rendant concrets les op´rateurs et donnant a e un type pr´cis aux sous-arbres. e Supposons par exemple que i et j aient ´t´ d´clar´es de type entier. L’analyse s´mantique de l’arbre pr´c´dent ee e e e e e permet d’en d´duire, successivement : e – que le + est l’addition des entiers, puisque les deux op´randes sont entiers, et donc que le sous-arbre e chapeaut´ par le + repr´sente une valeur de type entier, e e – que le ∗ est la multiplication des flottants, puisqu’un op´rande est flottant40 , qu’il y a lieu de convertir e l’autre op´rande vers le type flottant, et que le sous-arbre chapeaut´ par ∗ repr´sente un objet de type e e e flottant, – que l’affectation qui coiffe l’arbre tout entier consiste donc en l’affectation d’un flottant ` un entier, et a qu’il faut donc ins´rer une op´ration de troncation flottant → entier ; en C, il en d´coule que l’arbre tout e e e entier repr´sente une valeur du type entier. e En d´finitive, le contrˆle de type transforme l’arbre pr´c´dent en quelque chose qui ressemble ` ceci : e o e e a affectation d’entiers ¨rr ¨¨ r ¨ r i flottant→entier multiplication flottante ¨rr ¨¨ r entier →flottant 3.14 addition enti`re e ¨r ¨r 200 j

4.1

Repr´sentation et reconnaissance des types e

Une partie importante du travail s´mantique qu’un compilateur fait sur un programme est e – pendant la compilation des d´clarations, construire des repr´sentations des types d´clar´s dans le proe e e e gramme, – pendant la compilation des instructions, reconnaˆ les types des objets intervenant dans les expressions. ıtre La principale difficult´ de ce travail est la complexit´ des structures ` construire et ` manipuler. En effet, e e a a dans les langages modernes les types sont d´finis par des proc´d´s r´cursifs qu’on peut composer ` volont´. Par e e e e a e exemple, en C on peut avoir des entiers, des adresses (ou pointeurs) d’entiers, des fonctions rendant des adresses d’entiers, des adresses de fonctions rendant des adresses d’entiers, des tableaux d’adresses de fonctions rendant des adresses d’entiers, etc. Cela peut aller aussi loin qu’on veut, l’auteur d’un compilateur doit se donner le moyen de repr´senter ces structures de complexit´ quelconque. e e Faute de temps, nous n’´tudierons pas cet aspect des compilateurs dans le cadre de ce cours. e
39 Le type d’une constante est donn´ par une convention lexicale (exemple : 200 repr´sente un entier, 3.14 un flottant), le type e e d’une variable ou le type rendu par une fonction est sp´cifi´ par la d´claration de la variable ou la fonction en question. e e e 40 Cela s’appelle la  r`gle du plus fort , elle est suivie par la plupart des langages : lorsque les op´randes d’une op´ration e e e arithm´tique ne sont pas de mˆme type, celui dont le type est le  plus fort  (plus complexe, plus pr´cis, plus ´tendu, etc.) tire ` e e e e a lui l’autre op´rande, provoquant une conversion de type. e

38

Ainsi, le compilateur que nous r´aliserons ` titre de projet ne traitera que les types entier et tableau d’entiers. e a Pour les curieux, voici n´anmoins une suggestion de structures de donn´es pour la repr´sentation des prine e e cipaux types du langage C dans un compilateur qui serait lui-mˆme41 ´crit en C : e e 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´sent´ par une structure ` deux champs : classe, dont la valeur est un code e e a conventionnel qui indique de quelle sorte de type il s’agit, et attributs, qui donne les informations n´cessaires e pour achever de d´finir le type. Attributs est un champ polymorphe (en C, une union) dont la structure d´pend e e de la valeur du champ classe : – si la classe est celle d’un type primitif, le champ attributs est sans objet,
une question troublante qui finit par apparaˆ ıtre dans tous les cours de compilation : peut-on ´crire le compilateur d’un e langage en utilisant le langage qu’il s’agit de compiler ? Malgr´ l’apparent paradoxe, la chose est tout ` fait possible. Il faut e a comprendre que la question n’est pas de savoir, par exemple, dans quel langage fut ´crit le tout premier compilateur de C, si tant e est qu’il y eut un jour un compilateur de C alors que la veille il n’y en avait pas – cela est le probl`me, peu int´ressant, de l’œuf e e et de la poule. La question est plutˆt de savoir si, de nos jours, on peut utiliser un compilateur (existant) de C pour r´aliser un o e compilateur (nouveau) de C. Pr´sent´e comme cela, la chose est parfaitement raisonnable. e e L` o` elle redevient troublante : ce nouveau compilateur de C une fois ´crit, on devra le valider. Pour cela, quel meilleur test a u e que de lui donner ` compiler... son propre texte source, puisqu’il est lui-mˆme ´crit en C ? Le r´sultat obtenu devra ˆtre un nouvel a e e e e ex´cutable identique ` celui du compilateur. On voit l` un des int´rˆts qu’il y a ` ´crire le compilateur d’un langage dans le langage e a a e e ae ` compiler : on dispose ipso facto d’un formidable test de validation : au terme d’un certain processus de construction (on dit a plutˆt bootstrap) on doit poss´der un compilateur C capable de compiler son propre texte source S et de donner un r´sultat C(S) o e e v´rifiant C(S) = C. e
41 Voici

39

– si le champ classe indique qu’il s’agit d’un type pointeur, alors le champ attributs pointe sur la description du type des objets point´s, e e – si la valeur du champ classe est tTableau alors il faut deux attributs pour d´finir le type : nombreElements, le nombre de cases du tableau, et typeElement, qui pointe sur la description du type commun des ´l´ments ee du tableau, – s’il s’agit d’un type structure, l’attribut est l’adresse d’une liste chaˆ ee dont chaque maillon contient un ın´ pointeur42 sur un descripteur de type, – enfin, si le champ classe indique qu’il s’agit d’un type fonction, alors le champ attribut se compose de l’adresse d’un descripteur de type (le type rendu par la fonction) et l’adresse d’une liste de types (les types des arguments de cette derni`re). e On facilite l’allocation 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´es, voici un exemple purement d´monstratif, la e e construction  ` la main  du descripteur correspondant au type de la variable d´clar´e, en C, comme suit : a e e struct { char *lexeme; int uniteLexicale; } motRes[N]; La variable est motRes (nous l’avons utilis´e ` la section 2.2.2), elle est d´clar´e comme un tableau de N ´l´ments e a e e ee qui sont des structures ` deux champs : un pointeur sur un caract`re et un entier. Voici le code qui construit a e un tel descripteur (point´, ` la fin de la construction, par la variable tmp2) : e a ... listeDescripteursTypes *pCour, *pSuiv; descripteurType *pTmp1, *pTmp2; /* maillon de liste d´crivant le type entier */ e pCour = malloc(sizeof(listeDescripteursTypes)); pCour->info = nouveau(tInt); pCour->suiv = NULL; /* maillon de liste d´crivant le type pointeur sur caract`re */ e e 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 ´t´ viable de faire que chaque maillon de la liste chaˆ ee contienne un descripteur de type, au lieu d’un pointeur e e ın´ sur un tel descripteur. Apparemment plus lourde ` g´rer, la solution adopt´e ici se r´v`le ` l’usage la plus souple. a e e e e a

40

Dans le mˆme ordre d’id´es, voici la construction manuelle du descripteur du type  matrice de N L × N C e e flottants  ou, plus pr´cis´ment,  tableau de N L ´l´ments qui sont des tableaux de N C flottants  (en C, cela e e ee s’´crit : float matrice[NL][NC] ;). A la fin de la construction le descripteur cherch´ est point´ par pTmp2 : e e e ... /* description d’une ligne, tableau de NC flottants: */ pTmp1 = nouveau(tTableau); pTmp1->attributs.casTableau.nombreElements = NC; pTmp1->attributs.casTableau.typeElement = nouveau(tFloat); /* description d’une matrice, tableau de NL lignes: */ pTmp2 = nouveau(tTableau); pTmp2->attributs.casTableau.nombreElements = NL; pTmp2->attributs.casTableau.typeElement = pTmp1; ... Enfin, pour donner encore un exemple de manipulation de ces structures de donn´es, voici un utilitaire e fondamental dans les syst`mes de contrˆle de type : la fonction bool´enne qui fournit la r´ponse ` la question e o e e a  deux descripteurs donn´s d´crivent-ils des types identiques ?  : e e 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 ˆtre d´clar´es avant e e e d’ˆtre utilis´es dans les instructions. Quel que soit le degr´ de complexit´ des types support´s par notre come e e e e pilateur, celui-ci devra g´rer une table de symboles, appel´e aussi dictionnaire, dans laquelle se trouveront les e e identificateurs couramment d´clar´s, chacun associ´ ` certains attributs, comme son type, son adresse 43 et e e e a d’autres informations, cf. figure 10.

identif

type
tEntier

adresse complemt
180

v i t e s s e \0

Fig. 10 – Une entr´e dans le dictionnaire e Nous ´tudions pour commencer le cahier des charges du dictionnaire, c’est-`-dire les services que le compie a lateur en attend, puis, dans les sections suivantes, diverses impl´mentations possibles. e Grosso modo le dictionnaire fonctionne ainsi : – quand le compilateur trouve un identificateur dans une d´claration, il le cherche dans le dictionnaire en e esp´rant ne pas le trouver (sinon c’est l’erreur  identificateur d´j` d´clar´ ), puis il l’ajoute au dictionnaire e ea e e avec le type que la d´claration sp´cifie, e e – quand le compilateur trouve un identificateur dans la partie ex´cutable44 d’un programme, il le cherche e dans le dictionnaire avec l’espoir de le trouver (sinon c’est l’erreur  identificateur non d´clar´ ), ensuite e e il utilise les informations que le dictionnaire associe ` l’identificateur. a Nous allons voir que la question est un peu plus compliqu´e que cela. e 4.2.1 Dictionnaire global & dictionnaire local

Dans les langages qui nous int´ressent, un programme est essentiellement une collection de fonctions, e entre lesquelles se trouvent des d´clarations de variables. A l’int´rieur des fonctions se trouvent ´galement e e e des d´clarations de variables. e Les variables d´clar´es entre les fonctions et les fonctions elles-mˆmes sont des objets globaux. Un objet global e e e est visible45 depuis sa d´claration jusqu’` la fin du texte source, sauf aux endroits o` un objet local de mˆme e a u e nom le masque, voir ci-apr`s. e Les variables d´clar´es ` l’int´rieur des fonctions sont des objets locaux. Un objet local est visible dans la e e a e fonction o` il est d´clar´, depuis sa d´claration jusqu’` la fin de cette fonction ; il n’est pas visible depuis les u e e e a autres fonctions. En tout point o` il est visible, un objet local masque 46 tout ´ventuel objet global qui aurait u e le mˆme nom. e En d´finitive, quand le compilateur se trouve dans47 une fonction il faut poss´der deux dictionnaires : un e e dictionnaire global, contenant les noms des objets globaux couramment d´clar´s, et un dictionnaire local dans e e lequel se trouvent les noms des objets locaux couramment d´clar´s (qui, parfois, masquent des objets dont les e e noms se trouvant dans le dictionnaire global). Dans ces conditions, l’utilisation des dictionnaires que fait le compilateur se pr´cise : e
question des adresses des objets qui se trouvent dans les programmes sera ´tudi´e en d´tail ` la section 5.1.1. e e e a les langages comme C, Java, etc., la partie ex´cutable des programmes est l’ensemble des corps des fonctions dont le e programme se compose. En Pascal il faut ajouter ` cela le corps du programme. a 45 On dit qu’un objet o ayant le nom n est visible en un point d’un programme si une occurrence de n en ce point est comprise comme d´signant o. Cela ne pr´juge en rien de la correction ou de la l´galit´ de l’emploi de n en ce point. e e e e 46 Par masquage d’un objet o par un objet o de mˆme nom n on veut dire que o n’est pas alt´r´ ni d´truit, mais devient e e e e inaccessible car, dans la portion de programme o` le masquage a lieu, n d´signe o , non o. u e 47 Le compilateur lit le programme ` compiler s´quentiellement, du d´but vers la fin. A tout instant il en est ` un certain endroit a e e a du texte source, correspondant ` la position de l’unit´ lexicale courante ; quand la compilation progresse, l’unit´ lexicale avance. a e e Tout cela justifie un langage imag´, que nous allons employer, avec des expressions comme  le compilateur entre dans la partie e ex´cutable  ou  le compilateur entre (ou sort) d’une fonction , etc. e
44 Dans 43 La

42

– Lorsque le compilateur traite la d´claration d’un identificateur i qui se trouve ` l’int´rieur d’une fonction, e a e i est recherch´ dans le dictionnaire local exclusivement ; normalement, il ne s’y trouve pas (sinon,  erreur : e identificateur d´j` d´clar´ ). Suite ` cette d´claration, i est ajout´ au dictionnaire local. ea e e a e e Il n’y a strictement aucun int´rˆt ` savoir si i figure ` ce moment-l` dans le dictionnaire global. ee a a a – Lorsque le compilateur traite la d´claration d’un identificateur i en dehors de toute fonction, i est recherch´ e e dans le dictionnaire global, qui est le seul dictionnaire existant en ce point ; normalement, il ne s’y trouve pas (sinon,  erreur : identificateur d´j` d´clar´ ). Suite ` cette d´claration, i est ajout´ au dictionnaire ea e e a e e global. – Lorsque le compilateur compile une instruction ex´cutable, forc´ment ` l’int´rieur d’une fonction, chaque e e a e identificateur i rencontr´ est recherch´ d’abord dans le dictionnaire local ; s’il ne s’y trouve pas, il est e e recherch´ ensuite dans le dictionnaire global (si les deux recherches ´chouent,  erreur : identificateur non e e d´clar´ ). En proc´dant ainsi on assure le masquage des objets globaux par les objets locaux. e e e – Lorsque le compilateur quitte une fonction, le dictionnaire local en cours d’utilisation est d´truit, puisque e les objets locaux ne sont pas visibles ` l’ext´rieur de la fonction. Un dictionnaire local nouveau, vide, est a e cr´´ lorsque le compilateur entre dans une fonction. ee Notez ceci : tout au long d’une compilation le dictionnaire global ne diminue jamais. A l’int´rieur d’une e fonction il n’augmente pas ; le dictionnaire global n’augmente que lorsque le dictionnaire local n’existe pas. 4.2.2 Tableau ` acc`s s´quentiel a e e

L’impl´mentation la plus simple des dictionnaires consiste en un tableau dans lequel les identificateurs sont e plac´s dans l’ordre o` leurs d´clarations ont ´t´ trouv´es dans le texte source. Dans ce tableau, les recherches e u e ee e sont s´quentielles. Voyez la figure 11 : lorsqu’il existe, le dictionnaire local se trouve au-dessus du dictionnaire e 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 ` l’int´rieur (a) et ` l’ext´rieur (b) des fonctions a e a e Trois variables sont essentielles dans la gestion du dictionnaire : maxDico est le nombre maximum d’entr´es possibles (` ce propos, voir  Augmentation de la taille du e a dictionnaire  un peu plus loin), e sommet est le nombre d’entr´es valides dans le dictionnaire ; on doit avoir sommet ≤ maxDico, ee a base est le premier ´l´ment du dictionnaire du dessus (c’est-`-dire le dictionnaire local quand il y en a deux, le dictionnaire global quand il n’y en a qu’un). Avec tout cela, la manipulation du dictionnaire devient tr`s simple. Les op´rations n´cessaires sont : e e e 1. Recherche d’un identificateur pendant le traitement d’une d´claration (que ce soit ` l’int´rieur d’une e a e fonction ou ` l’ext´rieur de toute fonction) : rechercher l’identificateur dans la portion de tableau comprise a e entre les bornes sommet − 1 et base, e 2. Recherche d’un identificateur pendant le traitement d’une expression ex´cutable : rechercher l’identificateur en parcourant dans le sens des indices d´croissants 48 la portion de tableau comprise entre les bornes e sommet − 1 et 0,
48 En

parcourant le tableau du haut vers le bas on assure le masquage d’un objet global par un objet local de mˆme nom. e

43

3. Ajout d’une entr´e dans le dictionnaire (que ce soit ` l’int´rieur d’une fonction ou ` l’ext´rieur de toute e a e a e fonction) : apr`s avoir v´rifi´ que sommet < maxDico, placer la nouvelle entr´e ` la position sommet, e e e e a puis faire sommet ← sommet + 1, 4. Creation d’un dictionnaire local, au moment de l’entr´e dans une fonction : faire base ← sommet, e 5. Destruction du dictionnaire local, ` la sortie d’une fonction : faire sommet ← base puis base ← 0. a Augmentation de la taille du dictionnaire. Une question technique assez aga¸ante qu’il faut r´gler c e lors de l’impl´mentation d’un dictionnaire par un tableau est le choix de la taille ` donner ` ce tableau, ´tant e a a e entendu qu’on ne connaˆ pas ` l’avance la grosseur (en nombre de d´clarations) des programmes que notre ıt a e compilateur devra traiter. La biblioth`que C offre un moyen pratique pour r´soudre ce probl`me, la fonction realloc qui permet d’auge e e menter la taille d’un espace allou´ dynamiquement tout en pr´servant le contenu de cet espace. Voici, ` titre e e a d’exemple, la d´claration et les fonctions de gestion d’un dictionnaire r´alis´ dans un tableau ayant au d´part la e e e e place pour 50 ´l´ments ; chaque fois que la place manque, le tableau est agrandi d’autant qu’il faut pour loger ee 25 nouveaux ´l´ments : ee #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´moire)"); e 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´moire)"); e } 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´e au dictionnaire : e 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´moire)"); e strcpy(dico[sommet].identif, identif); dico[sommet].type = type; dico[sommet].adresse = adresse; dico[sommet].complement = complement; sommet++; } 4.2.3 Tableau tri´ et recherche dichotomique e

L’impl´mentation des dictionnaires expliqu´e ` la section pr´c´dente est facile ` mettre en œuvre et suffisante e e a e e a pour des applications simples, mais pas tr`s efficace (la complexit´ des recherches est, en moyenne, de l’ordre de e e n e 2 , soit O(n) ; les insertions se font en temps constant). Dans la pratique on recherche des impl´mentations plus efficaces, car un compilateur passe beaucoup de temps49 ` rechercher des identificateurs dans les dictionnaires. a Une premi`re am´lioration des dictionnaires consiste ` maintenir des tableaux ordonn´s, permettant des e e a e recherches par dichotomie (la complexit´ d’une recherche devient ainsi O(log2 n), c’est beaucoup mieux). La e figure 11 est toujours valable, mais maintenant il faut imaginer que les ´l´ments d’indices allant de base ` ee a sommet − 1 et, lorsqu’il y a lieu, ceux d’indices allant de 0 ` base − 1, sont plac´s en ordre croissant des a e identificateurs. Dans un tel contexte, voici la fonction existe, qui effectue la recherche de l’identificateur repr´sent´ par ident e e dans la partie du tableau, suppos´ ordonn´, comprise entre les indices inf et sup, bornes incluses. Le r´sultat de e e e la fonction (1 ou 0, interpr´t´s comme vrai ou faux ) est la r´ponse ` la question  l’´l´ment cherch´ se trouve-t-il ee e a ee e dans le tableau ? . En outre, au retour de la fonction, la variable point´e par ptrPosition contient la position e de l’´l´ment recherch´, c’est-`-dire : ee e a – si l’identificateur est dans le tableau, l’indice de l’entr´e correspondante, e – si l’identificateur ne se trouve pas dans le tableau, l’indice auquel il faudrait ins´rer, les cas ´ch´ant, une e e e entr´e concernant cet identificateur. e 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´e au dictionnaire : e 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 d’une compilation est d´pens´ en recherches dans les dictionnaires. e e

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´moire)"); e strcpy(dico[position].identif, identif); dico[position].type = type; dico[position].adresse = adresse; dico[position].complement = complt; } La fonction ajouterEntree utilise un param`tre position dont la valeur provient de la fonction existe. Pour e fixer les id´es, voici la fonction qui traite la d´claration d’un objet local : e e ... int placement; ... if (existe(lexeme, base, sommet - 1, &placement)) erreurFatale("identificateur d´j` d´clar´"); e a e e else { ici se place l’obtention des informations type, adresse, complement, etc. ajouterEntree(placement, lexeme, type, adresse, complement); } ... Nous constatons que l’utilisation de tableaux tri´s permet d’optimiser la recherche, dont la complexit´ passe e e de O(n) ` O(log2 n), mais p´nalise les insertions, dont la complexit´ devient O(n), puisqu’` chaque insertion il a e e a faut pousser d’un cran la moiti´ (en moyenne) des ´l´ments du dictionnaire. Or, pendant la compilation d’un e ee programme il y a beaucoup d’insertions et on ne peut pas n´gliger a priori le poids des insertions dans le calcul e du coˆt de la gestion des identificateurs. u Il y a cependant une tˆche, qui n’est pas la gestion du dictionnaire mais lui est proche, o` on peut sans a u r´serve employer un tableau ordonn´, c’est la gestion d’une table de mots r´serv´s, comme celle de la section e e e e 2.2.2. En effet, le compilateur, ou plus pr´cis´ment l’analyseur lexical, fait de nombreuses recherches dans cette e e 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´es arbre binaire. e Les arbres binaires de recherche ont les avantages des tables ordonn´es, pour ce qui est de l’efficacit´ de la e e recherche, sans leurs inconv´nients puisque, ´tant des structures chaˆ ees, l’insertion d’un ´l´ment n’oblige pas e e ın´ ee a ` pousser ceux qui d’un point de vue logique se placent apr`s lui. e Un arbre binaire de recherche, ou ABR, est un arbre binaire ´tiquet´ par des valeurs appartenant ` un e e a ensemble ordonn´, v´rifiant la propri´t´ suivante : pour tout nœud p de l’arbre e e ee – pour tout nœud q appartenant au sous-arbre gauche de p on a q→inf o ≤ p→inf o, – pour tout nœud r appartenant au sous-arbre droit de p on a r→inf o ≥ p→inf o. Voici, par exemple, l’ABR obtenu avec les  identificateurs  Denis, Fernand, Bernard, Andr´, Gaston, Ernest e et Charles, ajout´s ` l’arbre successivement et dans cet ordre50 : e a Denis ¨r ¨rr ¨

¨¨ Bernard ¨r ¨r ¨ r Andr´ Charles e

rr Fernand ¨r ¨¨ rr Ernest Gaston

50 Le nombre d’´l´ments et, surtout, l’ordre d’insertion font que cet ABR est parfaitement ´quilibr´. En pratique, les choses ne ee e e se passent pas aussi bien.

46

Techniquement, un tel arbre serait r´alis´ dans un programme comme le montre la figure 12. e e
Denis autres infos

Bernard autres infos autres infos

Fernand

...

...

...

Fig. 12 – R´alisation effective des maillons d’un ABR e Pour r´aliser les dictionnaires par des ABR il faudra donc se donner les d´clarations : e e typedef struct noeud { ENTREE_DICO info; struct noeud *gauche, *droite; } NOEUD; NOEUD *dicoGlobal = NULL, *dicoLocal = NULL; Voici la fonction qui recherche le nœud correspondant ` un identificateur donn´. Elle rend l’adresse du nœud a e cherch´, ou NULL en cas d’´chec de la recherche : e e 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´e pour rechercher des identificateurs apparaissant dans les parties ex´cutables des e e fonctions. Elle sera donc appel´e de la mani`re suivante : e e ... p = rechercher(lexeme, dicoLocal); if (p == NULL) { p = recherche(lexeme, dicoGlobal); if (p == NULL) erreurFatale("identificateur non d´clar´"); e e } exploitation des informations du nœud point´ par p e ... Pendant la compilation des d´clarations, les recherches se font avec la fonction suivante, qui effectue la e recherche et, dans la foul´e, l’ajout d’un nouveau nœud. Dans cette fonction, la rencontre d’un nœud associ´ e e a ` l’identificateur qu’on cherche est consid´r´e comme une erreur grave. La fonction rend l’adresse du nœud ee nouvelle cr´´ : ee 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´clar´"); e 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 d’appel (cas des d´clarations locales) : e ... 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 l’insertion est pass´ par adresse, c’est pourquoi il y a deux ∗∗ dans la d´claration NOEUD **adrDico. Cela ne sert qu’` couvrir e e a le cas de la premi`re insertion, lorsque le dictionnaire est vide : le pointeur point´ par adrDico (en pratique e e il s’agit soit de dicoLocal soit de dicoGlobal ) vaut NULL et doit changer de valeur. C’est beaucoup de travail pour pas grand-chose, on l’´viterait en d´cidant que les dictionnaires ne sont jamais vides (il suffit de leur cr´er e e e d’office un  nœud bidon  sans signification). ´ Restitution de l’espace alloue. L’impl´mentation d’un dictionnaire par un ABR poss`de l’efficacit´ de e e e la recherche dichotomique, car ` chaque comparaison on divise par deux la taille de l’ensemble susceptible de a contenir l’´l´ment cherch´, sans ses inconv´nients, puisque le temps n´cessaire pour faire une insertion dans un ee e e e ABR est n´gligeable. H´las, cette m´thode a deux d´fauts : e e e e – la recherche n’est dichotomique que si l’arbre est ´quilibr´, ce qui ne peut ˆtre suppos´ que si les identifie e e e cateurs sont tr`s nombreux et uniform´ment r´partis, e e e – la destruction d’un dictionnaire est une op´ration beaucoup plus complexe que dans les m´thodes qui e e utilisent un tableau. La destruction d’un dictionnaire, en l’occurrence le dictionnaire local, doit se faire chaque fois que le compilateur sort d’une fonction. Cela peut se programmer de la mani`re suivante : e void liberer(NOEUD *dico) { if (dico != NULL) { liberer(dico->gauche); liberer(dico->droite); free(dico); } } Comme on le voit, pour rendre l’espace occup´ par un ABR il faut le parcourir enti`rement (alors que dans e e le cas d’un tableau la modification d’un index suffit). Il y a un moyen de rendre beaucoup plus simple la lib´ration de l’espace occup´ par un arbre. Cela consiste e e ae ` ´crire sa propre fonction d’allocation, qu’on utilise ` la place malloc, et qui alloue un espace dont on maˆ a ıtrise la remise ` z´ro. Par exemple : a e 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`re technique de gestion d’une table de symboles m´rite d’ˆtre mentionn´e ici, car elle est tr`s e e e e e utilis´e dans les compilateurs r´els. Cela s’appelle adressage dispers´, ou hash-code 51 . Le principe en est assez e e e simple : au lieu de rechercher la position de l’identificateur dans la table, on obtient cette position par un calcul sur les caract`res de l’identificateur ; si on s’en tient ` des op´rations simples, un calcul est certainement plus e a e rapide qu’une recherche. Soit I l’ensemble des identificateurs existant dans un programme, N la taille de la table d’identificateurs. L’id´al serait de poss´der une fonction h : I → [0, N [ qui serait e e – rapide, facile ` calculer, a – injective, c’est-`-dire qui ` deux identificateurs distincts ferait correspondre deux valeurs distinctes. a a On ne dispose g´n´ralement pas d’une telle fonction car l’ensemble I des identificateurs pr´sents dans le e e e programme n’est pas connu a priori. De plus, la taille de la table n’est souvent pas suffisante pour permettre l’injectivit´ (qui requiert N ≥ |I|). e On se contente donc d’une fonction h prenant, sur l’ensemble des identificateurs possibles, des valeurs uniform´ment r´parties sur l’intervalle [0, N [. C’est-`-dire que h n’est pas injective, mais e e a – si N ≥ |I|, on esp`re que les couples d’identificateurs i1 , i2 tels que i1 = i2 et h(i1 ) = h(i2 ) (on appelle e cela une collision) sont peu nombreux, – si N < |I|, les collisions sont in´vitables. Dans ce cas on souhaite qu’elles soient ´galement r´parties : pour e e e |I| chaque j ∈ [0, N [ le nombre de i ∈ I tels que h(i) = j est ` peu pr`s le mˆme, c’est-`-dire N . Il est facile a e e a de voir pourquoi : h ´tant la fonction qui  place  les identificateurs dans la table, il s’agit d’´viter que e e ces derniers s’amoncellent ` certains endroits de la table, tandis qu’` d’autres endroits cette derni`re est a a e peu remplie, voire pr´sente des cases vides. e Il est difficile de dire ce qu’est une bonne fonction de hachage. La litt´rature sp´cialis´e rapporte de nome e e breuses recettes, mais il n’y a probablement pas de solution universelle, car une fonction de hachage n’est bonne que par rapport ` un ensemble d’identificateurs donn´. Parmi les conseils qu’on trouve le plus souvent : a e – prenez N premier (une des recettes les plus donn´es, mais plus personne ne se donne la peine d’en rappeler e la justification), – utilisez des fonctions qui font intervenir tous les caract`res des identificateurs ; en effet, dans les proe grammes 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`res ne serait pas tr`s bonne ici, e e e e e a – ´crivez des fonctions qui donnent comme r´sultat de grandes valeurs ; lorsque ces valeurs sont ramen´es ` l’intervalle [0, N [, par exemple par une op´ration de modulo, les ´ventuels d´fauts (dissym´tries, accumue e e e lations, etc.) de la fonction initiale ont tendance ` disparaˆ a ıtre. Une fonction assez souvent utilis´e consiste ` consid´rer les caract`res d’un identificateur comme les coeffie a e e cients d’un polynˆme P (X) dont on calcule la valeur pour un X donn´ (ou, ce qui revient au mˆme, ` voir un o e e a
51 La technique expliqu´e ici est celle dite adressage dispers´ ouvert. Il en existe une autre, l’adressage dispers´ ferm´, dans laquelle e e e e toute l’information se trouve dans le tableau adress´ par la fonction de hachage (il n’y a pas de listes chaˆ ees associ´s aux cases e ın´ e du tableau).

49

identificateur comme l’´criture d’un nombre dans une certaine base). En C, cela donne la fonction : e 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´ ouvert e L` o` les m´thodes d’adressage dispers´ diff´rent entre elles c’est dans la mani`re de g´rer les collisions. a u e e e e e Dans le cas de l’adressage dispers´ ouvert, la table qu’on adresse ` travers la fonction de hachage n’est pas une e a table d’identificateurs, mais une table de listes chaˆ ees dont les maillons portent des identificateurs (voyez la ın´ figure 13). Si on note T cette table, alors Tj est le pointeur de tˆte de la liste chaˆ ee dans laquelle sont les e ın´ identificateurs i tels que h(i) = j. Vu de cette mani`re, l’adressage dispers´ ouvert apparaˆ comme une m´thode de partitionnement de l’ene e ıt e semble des identificateurs. Chaque liste chaˆ ee est un compartiment de la partition. Si la fonction h est bien ın´ faite, les compartiments ont ` peu pr`s la mˆme taille. L’efficacit´ de la m´thode provient alors du fait qu’au a e e e e lieu de faire une recherche dans une structure de taille |I| on fait un calcul et une recherche dans une structure de taille |I| . 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 l’insertion d’un identificateur (suppos´ absent) : e MAILLON *insertion(char *identif) { int h = hash(identif, N); 50

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

5

Production de code

Nous nous int´ressons ici ` la derni`re phase de notre compilateur, la production de code. Dans ce but, e a e nous pr´sentons certains aspects de l’architecture des machines (registres et m´moire, adresses, pile d’ex´cution, e e e compilation s´par´e et ´dition de liens, etc.) et en mˆme temps nous introduisons une machine virtuelle, la e e e e machine Mach 1, pour laquelle notre compilateur produit du code et dont nous ´crirons un simulateur. e Faute de temps, nous faisons l’impasse sur certains aspects importants (et difficiles) de la g´n´ration de code, e e et notamment sur les algorithmes pour l’optimisation du code et pour l’allocation des registres.

5.1
5.1.1

Les objets et leurs adresses
Classes d’objets

Dans les langages qui nous int´ressent les programmes manipulent trois classes d’objets : e 1. Les objets statiques existent pendant toute la dur´e de l’ex´cution d’un programme ; on peut consid´rer e e e que l’espace qu’ils occupent est allou´ par le compilateur pendant la compilation52 . e 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´e par l’auteur du programme (dans les langages qui le permettent) ou bien une e valeur initiale implicite, souvent z´ro. e Les objets statiques peuvent ˆtre en lecture seule ou en lecture-´criture 53 . Les fonctions et les constantes e e sont des objets statiques en lecture seule. Les variables globales sont des objets statiques en lecture-´criture. e On appelle espace statique l’espace m´moire dans lequel sont log´s les objets statiques. Il est g´n´ralement e e e e constitu´ de deux zones : la zone du code, o` sont les fonctions et les constantes, et l’espace global, o` sont e u u les variables globales. L’adresse d’un objet statique est un nombre entier qui indique la premi`re (parfois l’unique) cellule de e la m´moire occup´e par l’objet. Elle est presque toujours exprim´e comme un d´calage54 par rapport au e e e e d´but de la zone contenant l’objet en question. e 2. Les objets automatiques sont les variables locales des fonctions, ce qui comprend :
52 En r´alit´, le compilateur n’alloue pas la m´moire pendant la compilation ; au lieu de cela, il en produit une certaine repr´sene e e e tation dans le fichier objet, et c’est un outil appel´ chargeur qui, apr`s traitement du fichier objet par l’´diteur de lien, installe les e e e objets statiques dans la m´moire. Mais, pour ce qui nous occupe ici, cela revient au mˆme. e e 53 Lorsque le syst`me le permet, les objets en lecture seule sont log´s dans des zones de la m´moire dans lesquelles les tentatives e e e d’´criture, c’est-`-dire de modification de valeurs, sont d´tect´es et signal´es ; les petits syst`mes n’offrent pas cette s´curit´ (qui e a e e e e e e limite les d´gˆts lors des utilisations inad´quates des pointeurs et des indices des tableaux). e a e 54 Le mot d´calage (les anglophones disent offset) fait r´f´rence ` une m´thode d’adressage employ´e tr`s souvent : une entit´ est e ee a e e e e rep´r´e par un couple (base,d´calage), o` base est une adresse connue et d´calage un nombre entier qu’il faut ajouter ` base pour e e e u e a obtenir l’adresse voulue. L’acc`s t[i] au i-`me ´l´ment d’un tableau t en est un exemple typique, dans lequel t (l’adresse de t[0]) e e ee est la base et i le d´calage. e

51

– les variables d´clar´es ` l’int´rieur des fonctions, e e a e – les arguments formels de ces derni`res. e Ces variables occupent un espace qui n’existe pas pendant toute la dur´e de l’ex´cution du programme, e e mais uniquement lorsqu’il est utile. Plus pr´cis´ment, l’activation d’une fonction commence par l’allocation e e d’un espace, appel´ espace local de la fonction, de taille suffisante pour contenir ses arguments et ses e variables locales (plus un petit nombre d’informations techniques additionnelles expliqu´es en d´tail ` la e e a section 5.2.4). Un espace local nouveau est allou´ chaque fois qu’une fonction est appel´e, mˆme si cette fonction ´tait e e e e d´j` active et donc qu’un espace local pour elle existait d´j` (c’est le cas d’une fonction qui s’appelle ea ea elle-mˆme, directement ou indirectement). Lorsque l’activation d’une fonction se termine son espace local e est d´truit. e

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

Fig. 14 – Empilement d’espaces locaux (f a appel´ g qui a appel´ f qui a appel´ h) e e e La propri´t´ suivante est importante : chaque fois qu’une fonction se termine, la fonction qui se termine ee est la plus r´cemment activ´e de celles qui ont ´t´ commenc´es et ne sont pas encore termin´es. Il en e e ee e e d´coule que les espaces locaux des fonctions peuvent ˆtre allou´s dans une m´moire g´r´e comme une pile e e e e ee (voyez la figure 14) : lorsqu’une fonction est activ´e, son espace local est cr´´ au-dessus des espaces locaux e ee des fonctions actives (i.e. commenc´es et non termin´es) ` ce moment-l`. Lorsqu’une fonction se termine, e e a a son espace local est celui qui se trouve au sommet de la pile et il suffit de le d´piler pour avoir au sommet e l’espace local de la fonction qui va reprendre le contrˆle. o 3. Les objets dynamiques sont allou´s lorsque le programme le demande explicitement (par exemple ` travers e a la fonction malloc de C ou l’op´rateur new de Java et C++). Si leur destruction n’est pas explicitement e demand´e (fonction free de C, op´rateur delete de C++) ces objets existent jusqu’` la terminaison du e e a programme55 . Les objets dynamiques ne peuvent pas ˆtre log´s dans les zones o` sont les objets statiques, puisque leur e e u existence n’est pas certaine, elle d´pend des conditions d’ex´cution. Ils ne peuvent pas davantage ˆtre e e e h´berg´s dans la pile des espaces locaux, puisque cette pile grandit et diminue en accord avec les appels e e et retours des fonctions, alors que les moments de la cr´ation et de la destruction des objets dynamiques e ne sont pas connus a priori. On place donc les objets dynamiques dans un troisi`me espace, distinct des e deux pr´c´dents, appel´ le tas (heap, en anglais). e e e La gestion du tas, c’est-`-dire son allocation sous forme de morceaux de tailles variables, ` la demande du a a programme, la r´cup´ration des morceaux rendus par ce dernier, la lutte contre l’´miettement de l’espace e e e disponible, etc. font l’objet d’algorithmes savants impl´ment´s dans des fonctions comme malloc et free e e ou des op´rateurs comme new et delete. e En d´finitive, la m´moire utilis´e par un programme en cours d’ex´cution est divis´e en quatre espaces (voyez e e e e e la figure 15) :
55 Dans certains langages, comme Java, c’est un m´canisme de r´cup´ration de la m´moire inutilis´e qui se charge de d´truire les e e e e e e objets dynamiques dont on peut prouver qu’ils n’interviendront 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´moire e

– – – –

le code (espace statique en lecture seule), contenant le programme et les constantes, l’espace global (espace statique en lecture-´criture), contenant les variables globales, e la pile (stack ), contenant variables locales, le tas (heap), contenant les variables allou´es dynamiquement. e

Les tailles du code et de l’espace global sont connues d`s la fin de la compilation et ne changent pas pendant e l’ex´cution du programme. La pile et le tas, en revanche, ´voluent au cours de l’ex´cution : le tas ne fait que e e e grossir56 , la pile grossit lors des appels de fonctions et diminue lors des terminaisons des fonctions appel´es. e La rencontre de la pile et du tas (voyez la figure 15) est un accident mortel pour l’ex´cution du programme. e Cela se produit lorsqu’un programme alloue trop d’objets dynamiques et/ou de taille trop importante, ou bien lorsqu’un programme fait trop d’appels de fonctions et/ou ces derni`res ont des espaces locaux de taille trop e importante. 5.1.2 D’o` viennent les adresses des objets ? u

Puisque l’adresse d’un objet est un d´calage par rapport ` la base d’un certain espace qui d´pend de la e a e classe de l’objet, du point de vue du compilateur  obtenir l’adresse d’un objet  c’est simplement m´moriser e l’´tat courant d’un certain compteur dont la valeur exprime le niveau de remplissage de l’espace correspondant. e Cela suppose que d’autres op´rations du compilateur font par ailleurs grandir ce compteur, de sorte que si un e peu plus tard on cherche ` obtenir l’adresse d’un autre objet on obtiendra une valeur diff´rente. a e Plus pr´cis´ment, pendant qu’il produit la traduction d’un programme, le compilateur utilise les trois vae e riables suivantes : e TC (Taille du Code) Pendant la compilation, cette variable a constamment pour valeur le nombre d’unit´s (des octets, des mots, cela d´pend de la machine) du programme en langage machine couramment produites. e Au d´but de la compilation, TC vaut 0. Ensuite, si memoire repr´sente l’espace (ou le fichier) dans lequel e e le code machine est m´moris´, la production d’un ´l´ment de code, comme un opcode ou un op´rande, se e e ee e traduit par les deux affectations : memoire[T C] ← element TC ← TC + 1
56 En effet, le rˆle des fonctions de restitution de m´moire (free, delete) est de bien g´rer les parcelles de m´moire emprunt´e puis o e e e e rendue par un programme ; ces fonctions limitent la croissance du tas, mais elles ne sont pas cens´es en r´duire la taille. e e

53

Par cons´quent, pour m´moriser l’adresse d’une fonction fon il suffit de faire, au moment o` commence e e u la compilation de la fonction : adresse(f on) ← T C TEG (Taille de l’Espace Global) Cette variable a constamment pour valeur la somme des tailles des variables globales dont le compilateur a rencontr´ la d´claration. e e Au d´but de la compilation TEG vaut 0. Par la suite, le rˆle de TEG dans la d´claration d’une variable e o e globale varg se r´sume ` ceci : e a adresse(varg) ← T EG T EG ← T EG + taille(varg) o` taille(varg) repr´sente la taille de la variable en question, qui d´pend de son type. u e e e TEL (Taille de l’Espace Local) A l’int´rieur d’une fonction, cette variable a pour valeur la somme des tailles des variables locales de la fonction en cours de compilation. Si le compilateur n’est pas dans une fonction TEL n’est pas d´fini. e A l’entr´e de chaque fonction TEL est remis ` z´ro. Par la suite, le rˆle de TEL dans la d´claration d’une e a e o e variable locale varl se r´sume ` ceci : e a adresse(varl) ← T EL T EL ← T EL + taille(varl) Les arguments formels de la fonction, bien qu’´tant des objets locaux, n’interviennent pas dans le calcul e de TEL (la question des adresses des arguments formels sera trait´e ` la section 5.2.4). e a A la fin de la compilation les valeurs des variables TC et TEG sont pr´cieuses : e – TC est la taille du code donc, dans une organisation comme celle de la figure 15, elle est aussi le d´calage e (relatif ` l’origine g´n´rale de la m´moire du programme) correspondant ` la base de l’espace global, a e e e a – TEG est la taille de l’espace global ; par cons´quent, dans une organisation comme celle de la figure 15, e T C + T EG est le d´calage (relatif ` l’origine g´n´rale de la m´moire du programme) correspondant au e a e e e niveau initial de la pile. 5.1.3 Compilation s´par´e et ´dition de liens e e e

Tout identificateur apparaissant dans une partie ex´cutable d’un programme doit avoir ´t´ pr´alablement e ee e d´clar´. La d´claration d’un identificateur i, que ce soit le nom d’une variable locale, d’une variable globale ou e e e d’une fonction, produit son introduction dans le dictionnaire ad´quat, associ´ ` une adresse, notons-la adri . Par e ea 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 d’un compilateur les identificateurs qui se trouvaient dans le texte source ont disparu57 et, de fait, tel peut ˆtre le cas dans les langages qui obligent ` mettre tout le e a programme dans un seul fichier. Mais les choses sont plus compliqu´es dans les langages, comme C ou Java, o` le texte d’un programme peut e u se trouver ´clat´ dans plusieurs fichiers sources destin´s ` ˆtre compil´s ind´pendamment les uns des autres e e e a e e e (on appelle cela la compilation s´par´e). En effet, dans ces langages il doit ˆtre possible qu’une variable ou une e e e fonction d´clar´e dans un fichier soit mentionn´e dans un autre. Cela implique qu’` la fin de la compilation il e e e a y a dans le fichier produit quelque trace des noms des variables et fonctions mentionn´es dans le fichier source. e Notez que cette question ne concerne que les objets globaux. Les objets locaux, qui ne sont d´j` pas visibles ea en dehors de la fonction dans laquelle ils sont d´clar´s, ne risquent pas d’ˆtre visibles dans un autre fichier. e e e Notez ´galement que le principal int´ress´ par cette affaire n’est pas le compilateur, mais un outil qui lui e e e est associ´, l’´diteur de liens (ou linker ) dont le rˆle est de concat´ner plusieurs fichiers objets, r´sultats de e e o e e compilations s´par´es, pour en faire un unique programme ex´cutable, en v´rifiant que les objets r´f´renc´s mais e e e e ee e non d´finis dans certains fichiers sont bien d´finis dans d’autres fichiers, et en compl´tant de telles  r´f´rences e e e ee insatisfaites  par les adresses des objets correspondants. Faute de temps, le langage dont nous ´crirons le compilateur ne supportera pas la compilation s´par´e. e e e Nous n’aurons donc pas besoin d’´diteur de liens dans notre syst`me. e e
57 Une cons´quence tangible de la disparition des identificateurs est l’impossibilit´ de  d´compiler  les fichiers objets, ce qui e e e n’est pas grave, mais aussi la difficult´ de les d´boguer, ce qui est plus embˆtant. C’est pourquoi la plupart des compilateurs ont e e e une option de compilation qui provoque la conservation des identificateurs avec le code, afin de permettre aux d´bogueurs d’acc´der e e aux variables et fonctions par leurs noms originaux.

54

Pour les curieux, voici n´anmoins quelques explications sur l’´diteur de liens et la structure des fichiers objets e e dans les langages qui autorisent la compilation s´par´e. e e Appelons module un fichier produit par le compilateur. Le rˆle de l’´diteur de liens est de concat´ner (mettre o e e bout ` bout) plusieurs modules. En g´n´ral, il concat`ne d’un cˆt´ les zones de code (objets statiques en lecture a e e e oe seule) de chaque module, et d’un autre cot´ les espaces globaux (objets statiques en lecture ´criture) de chaque e e module, afin d’obtenir une unique zone de code totale et un unique espace global total. Un probl`me apparaˆ imm´diatement : si on ne fait rien, les adresses des objets statiques, qui sont exprim´es e ıt e e comme des d´placements relatifs ` une base propre ` chaque module, vont devenir fausses, sauf pour le module e a a qui se retrouve en tˆte. C’est facile ` voir : chaque module apporte une fonction d’adresse 0 et probablement e a une variable globale d’adresse 0. Dans l’ex´cutable final, une seule fonction et une seule variable globale peuvent e avoir l’adresse 0. Il faut donc que l’´diteur de liens passe en revue tout le contenu des modules qu’il concat`ne et en corrige e e toutes les r´f´rences ` des objets statiques, pour tenir compte du fait que le d´but de chaque module ne ee a e correspond plus ` l’adresse 0 mais ` une adresse ´gale ` la somme des tailles des modules qui ont ´t´ mis devant a a e a ee lui. ´ ´ ´ ´ References absolues et references relatives. En pratique le travail mentionn´ ci-dessus se trouve e all´g´ par le fait que les langages-machines supportent deux mani`res de faire r´f´rence aux objets statiques. e e e ee On dit qu’une instruction fait une r´f´rence absolue ` un objet statique (variable ou fonction) si l’adresse ee a de ce dernier est indiqu´e par un d´calage relatif ` la base de l’espace concern´ (l’espace global ou l’espace du e e a e code). Nous venons de voir que ces r´f´rences doivent ˆtre corrig´es lorsque le module qui les contient est d´cal´ ee e e e e et ne commence pas lui-mˆme, dans le fichier qui sort de l’´diteur de liens, ` l’adresse 0. e e a On dit qu’une instruction fait une r´f´rence relative ` un objet statique lorsque ce dernier est rep´r´ par un ee a ee d´calage relatif ` l’adresse ` laquelle se trouve la r´f´rence elle-mˆme. e a a ee e
base

x

fonction A

fonction A

y
APPELA x APPELR y

Fig. 16 – R´f´rence absolue et r´f´rence relative ` une mˆme fonction A ee ee a e Par exemple, la figure 16 montre le cas d’un langage machine dans lequel il y a deux instructions pour appeler une fonction : APPELA, qui prend comme op´rande une r´f´rence absolue, et APPELR qui prend une r´f´rence e ee ee relative. L’int´rˆt des r´f´rences relatives saute aux yeux : elles sont insensibles aux d´calages du module dans lequel ee ee e elles se trouvent. Lorsque l’´diteur de liens concat`ne des modules pour former un ex´cutable, les r´f´rences e e e ee relatives contenues dans ces modules n’ont pas ` ˆtre mises ` jour. ae a Bien entendu, cela ne concerne que l’acc`s aux objets qui se trouvent dans le mˆme module que la r´f´rence ; e e ee il n’y a aucun int´rˆt ` repr´senter par une r´f´rence relative un acc`s ` un objet d’un autre module. Ainsi, une ee a e ee e a r`gle suivie par les compilateurs, lorsque le langage machine le permet, est : produire des r´f´rences intra-modules e ee relatives et des r´f´rences inter-modules absolues. ee Structure des modules. Il nous reste ` comprendre comment l’´diteur de liens arrive ` attribuer ` une a e a a r´f´rence ` un objet non d´fini dans un module (cela s’appelle une r´f´rence insatisfaite) l’adresse de l’objet, ee a e ee qui se trouve dans un autre module.

55

Pour cela il faut savoir que chaque fichier produit par le compilateur se compose de trois parties : une section de code et deux tables faites de couples (identificateur, adresse dans la section de code) : – la section de code contient la traduction en langage machine d’un fichier source ; ce code comporte un certain nombre de valeurs incorrectes, ` savoir les r´f´rences ` des objets externes (i.e. non d´finis dans ce a ee a e module) dont l’adresse n’est pas connue au moment de la compilation, – la table des r´f´rences insatisfaites de la section de code ; dans cette table, chaque identificateur r´f´renc´ ee ee e mais non d´fini est associ´ ` l’adresse de l’´l´ment de code, au contenu incorrect, qu’il faudra corriger e e a ee lorsque l’adresse de l’objet sera connue, – la table des objets publics 58 d´finis dans le module ; dans cette table, chaque identificateur est le nom d’un e objet que le module en question met ` la disposition des autres modules, et il est associ´ ` l’adresse de a ea l’objet concern´ dans la section de code . e
section de code références insatisfaites ... fonction g ...

x y
fonction f

y

... APPELA ...

???

objets publics ... fonction f ...

x

Fig. 17 – Un module objet Par exemple, la figure 17 repr´sente un module contenant la d´finition d’une fonction publique f et l’appel e e d’une fonction non d´finie dans ce module g. e En r´sum´, le travail de l’´diteur de liens se compose sch´matiquement des tˆches suivantes : e e e e a – concat´nation des sections de code des modules donn´s ` lier ensemble, e e a – d´calage des r´f´rences absolues contenues dans ces modules (dont les m´moris´es dans les tables), e ee e e – r´union des tables d’objets publics et utilisation de la table obtenue pour corriger les r´f´rences insatisfaites e ee pr´sentes dans les sections de code, e – ´mission du fichier ex´cutable final, form´ du code corrig´. e e e e

5.2

La machine Mach 1

Nous continuons notre expos´ sur la g´n´ration de code par la pr´sentation de la machine Mach 159 , la e e e e machine cible du compilateur que nous r´aliserons ` titre de projet. e a 5.2.1 Machines ` registres et machines ` pile a a

Les langages ´volu´s permettent l’´criture d’expressions en utilisant la notation alg´brique ` laquelle nous e e e e a sommes habitu´s, comme X = Y + Z, cette formule signifiant  ajoutez le contenu de Y ` celui de Z et rangez e a le r´sultat dans X  (X, Y et Z correspondent ` des emplacements dans la m´moire de l’ordinateur). e a e Une telle expression est trop compliqu´e pour le processeur, il faut la d´composer en des instructions plus e e simples. La nature de ces instructions plus simples d´pend du type de machine dont on dispose. Relativement e a ` la mani`re dont les op´rations sont exprim´es, il y a deux grandes familles de machines : e e e
58 Quels sont les objets publics, c’est-`-dire les objets d´finis dans un module qu’on peut r´f´rencer depuis d’autres modules ? a e ee On l’a d´j` dit, un objet public est n´cessairement global. Inversement, dans certains langages, tout objet global est public. Dans ea e d’autres langages, aucun objet n’est public par d´faut et il faut une d´claration explicite pour rendre publics les objets globaux que e e le programmeur souhaite. Enfin, dans des langages comme le C, tous les objets globaux sont publics par d´faut, et une qualification e sp´ciale (dans le cas de C c’est la qualification static) permet d’en rendre certains priv´s, c’est-`-dire non publics. e e a 59 Cela se prononce, au choix,  m`que ouane  ou  machun . e

56

1. Les machines ` registres poss`dent un certain nombre de registres, not´s ici R1, R2, etc., qui sont les a e e seuls composants susceptibles d’intervenir dans une op´rations (autre qu’un transfert de m´moire ` registre ou e e a r´ciproquement) ` titre d’op´randes ou de r´sultats. Inversement, n’importe quel registre peut intervenir dans e a e e une op´ration arithm´tique ou autre ; par cons´quent, les instructions qui expriment ces op´rations doivent e e e e sp´cifier leurs op´randes. Si on vise une telle machine, l’affectation X = Y + Z devra ˆtre traduite en quelque e e e 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´place la valeur de Y dans R1 e d´place la valeur de Z dans R2 e ajoute R1 ` R2 a d´place la valeur de R2 dans X e

2. Les machines ` pile, au contraire, disposent d’une pile (qui peut ˆtre la pile des variables locales) au a e sommet de laquelle se font toutes les op´rations. Plus pr´cis´ment : e e e – les op´randes d’un op´rateur binaire sont toujours les deux valeurs au sommet de la pile ; l’ex´cution e e e d’une op´ration binaire ⊗ consiste toujours ` d´piler deux valeurs x et y et ` empiler le r´sultat x ⊗ y de e a e a e l’op´ration, e – l’op´rande d’un op´rateur unaire est la valeur au sommet de la pile ; l’ex´cution d’une op´ration unaire e e e e consiste toujours ` d´piler une valeur x et ` empiler le r´sultat x de l’op´ration. a e a e e 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`ve la valeur au sommet de la pile et la range dans X e

Il est a priori plus facile de produire du code de bonne qualit´ pour une machine ` pile plutˆt que pour une e a o machine ` registres, mais ce d´faut est largement corrig´ dans les compilateurs commerciaux par l’utilisation a e e de savants algorithmes qui optimisent l’utilisation des registres. Car il faut savoir que les machines  physiques  existantes sont presque toujours des machines ` registres, a pour une raison d’efficacit´ facile ` comprendre : les op´rations qui ne concernent que des registres restent e a e internes au microprocesseur et ne sollicitent ni le bus ni la m´moire de la machine60 . Et, avec un compilae teur optimisateur, les expressions complexes sont traduites par des suites d’instructions dont la plupart ne mentionnent que des registres. Les algorithmes d’optimisation et d’allocation des registres sortant du cadre de ce cours, la machine pour laquelle nous g´n´rerons du code sera une machine ` pile. e e a 5.2.2 Structure g´n´rale de la machine Mach 1 e e

La m´moire de la machine Mach 1 est faite de cellules num´rot´es, organis´es comme le montre la figure e e e e 18 (c’est la structure de la figure 15, le tas en moins). Les registres suivants ont un rˆle essentiel dans le o fonctionnement de la machine : CO (Compteur Ordinal) indique constamment la cellule contenant l’instruction que la machine est en train d’ex´cuter, e BEG (Base de l’Espace Global) indique la premi`re cellule de l’espace r´serv´ aux variables globales (autree e e ment dit, BEG pointe la variable globale d’adresse 0), e BEL (Base de l’Espace Local) indique la cellule autour de laquelle est organis´ l’espace local de la fonction en cours d’ex´cution ; la valeur de BEL change lorsque l’activation d’une fonction commence ou finit (` ce e a propos voir la section 5.2.4), SP (Sommet de la Pile) indique constamment le sommet de la pile, ou plus exactement la premi`re cellule e libre au-dessus de la pile, c’est-`-dire le nombre total de cellules occup´es dans la m´moire. a e e
60 Se rappeler que de nos jours, d´cembre 2001, le microprocesseur d’un ordinateur personnel moyennement puissant tourne ` 2 e a GHz (c’est sa cadence interne) alors que son bus et sa m´moire ne travaillent qu’` 133 MHz. e a

57

espace disponible SP pile

BEL

espace global BEG CO code

Fig. 18 – Organisation de la m´moire de la machine Mach 1 e

5.2.3

Jeu d’instructions

La machine Mach 1 est une machine ` pile. a Chaque instruction est faite soit d’un seul opcode, elle occupe alors une cellule, soit d’un opcode et un op´rande, elle occupe alors deux cellules cons´cutives. Les constantes enti`res et les  adresses  ont la mˆme e e e e taille, qui est celle d’une cellule. La table 1, ` la fin de ce polycopi´, donne la liste des instructions. a e 5.2.4 Compl´ments sur l’appel des fonctions e

La cr´ation et la destruction de l’espace local des fonctions a lieu ` quatre moments bien d´termin´s : e a e e l’activation de la fonction, d’abord du point de vue de la fonction appelante (1), puis de celui de la fonction appel´e (2), ensuite le retour, d’abord du point de vue de la fonction appel´e (3) puis de celui de la fonction e e appelante (4). Voici ce qui se passe (voyez la figure 19) : 1. La fonction appelante r´serve un mot vide sur la pile, o` sera d´pos´ le r´sultat de la fonction, puis e u e e e elle empile les valeurs des arguments effectifs. Ensuite, elle ex´cute une instruction APPEL (qui empile e l’adresse de retour). 2. La fonction appel´e empile le  BEL courant  (qui est en train de devenir  BEL pr´c´dent ), prend la e e e valeur de SP pour BEL courant, puis alloue l’espace local. Pendant la dur´e de l’activation de la fonction : e – les variables locales sont atteintes ` travers des d´placements (positifs ou nuls) relatifs ` BEL : 0 ↔ premi`re a e a e variable locale, 1 ↔ deuxi`me variable locale, etc. e – les arguments formels sont ´galement atteints ` travers des d´placements (n´gatifs, cette fois) relatifs ` e a e e a BEL : −3 ↔ n-`me argument, −4 ↔ (n − 1)-`me argument, etc. e e u e e a e – la cellule o` la fonction doit d´poser son r´sultat est atteinte elle aussi ` travers un d´placement relatif a ` BEL. Ce d´placement est −(n + 3), n ´tant le nombre d’arguments formels, et suppose donc l’accord e e entre la fonction appelante et la fonction appel´e au sujet du nombre d’arguments de la fonction appel´e. e e L’ensemble de toutes ces valeurs forme l’ espace local  de la fonction en cours. Au-del` de cet espace, la a pile est utilis´e pour le calcul des expressions courantes. e 3. Terminaison du travail : la fonction appel´e remet BEL et SP comme ils ´taient lorsqu’elle a ´t´ activ´e, e e ee e puis effectue une instruction RETOUR. e e 4. Reprise du calcul interrompu par l’appel. La fonction appelante lib`re l’espace occup´ par les valeurs des arguments effectifs. Elle se retrouve alors avec une pile au sommet de laquelle est le r´sultat de la fonction e qui vient d’ˆtre appel´e. Globalement, la situation est la mˆme qu’apr`s une op´ration arithm´tique : les e e e e e e op´randes ont ´t´ d´pil´s et remplac´s par le r´sultat. e ee e e e e

58

haut de la pile

SP évaluation en cours var. loc. k ... var. loc. 1 BEL précédent adr. retour arg n ... arg 1 résultat espace local de la fonction appelante
bas de la pile

BEL

Fig. 19 – Espace local d’une fonction

´ Qui cree l’espace local d’une fonction ? Les arguments d’une fonction et les variables d´clar´es ` e e a l’int´rieur de cette derni`re sont des objets locaux et ont donc des adresses qui sont des d´placements relatifs ` e e e a BEL (positifs dans le cas des variables locales, n´gatifs dans le cas des arguments, voir la figure 19). L’explication e pr´c´dente montre qu’il y a une diff´rence importante entre ces deux sortes d’objets locaux : e e e – les arguments sont install´s dans la pile par la fonction appelante, car ce sont les valeurs d’expressions e qui doivent ˆtre ´valu´es dans le contexte de la fonction appelante. Il est donc naturel de donner ` cette e e e a derni`re la responsabilit´ de les enlever de la pile, au retour de la fonction appel´e, e e e – les variables locales sont allou´es par la fonction appel´e, qui est la seule ` en connaˆ le nombre ; c’est e e a ıtre donc cette fonction qui doit les enlever de la pile, lorsqu’elle se termine (l’instruction SORTIE fait ce travail). Les conventions d’appel. Il y a donc un ensemble de conventions, dites conventions d’appel, qui pr´cisent e a ` quel endroit la fonction appelante doit d´poser les valeurs des arguments et o` trouvera-t-elle le r´sultat ou, e u e ce qui revient au mˆme, ` quel endroit la fonction appel´e trouvera ses arguments et o` doit-elle d´poser son e a e u e r´sultat. e ´ En g´n´ral les conventions d’appel ne sont pas laiss´es ` l’appr´ciation des auteurs de compilateurs. Edict´es e e e a e e par les concepteurs du syst`me d’exploitation, elles sont suivies par tous les compilateurs  homologu´s , ce e e qui a une cons´quence tr`s importante : puisque tous les compilateurs produisent des codes dans lesquels les e e param`tres et le r´sultat des fonctions sont pass´s de la mˆme mani`re, une fonction ´crite dans un langage e e e e e e L1 , et compil´e donc par le compilateur de L1 , pourra appeler une fonction ´crite dans un autre langage L2 et e e compil´e par un autre compilateur, celui de L2 . e Nous avons choisi ici des conventions d’appel simples et pratiques (beaucoup de syst`mes font ainsi) puisque : e e e – les arguments sont empil´s dans l’ordre dans lequel ils se pr´sentent dans l’expression d’appel, – le r´sultat est d´pos´ l` o` il faut pour que, apr`s nettoyage des arguments, il soit au sommet de la pile. e e e a u e Mais il y a un petit inconv´nient61 . Dans notre syst`me, les argument arg1 , arg2 , ... argn sont atteints, ` e e a l’int´rieur de la fonction, par les adresses locales respectives −(n + 3) + 1, −(n + 3) + 2, ... −(n + 3) + n = −3, et e le r´sultat de la fonction lui-mˆme poss`de l’adresse locale −(n + 3). Cela oblige la fonction appel´e ` connaˆ e e e e a ıtre
61 Et encore, ce n’est pas sur que ce soit un inconv´nient, tellement il semble naturel d’exiger, comme en Pascal ou Java, que la e fonction appelante et la fonction appel´e soient d’accord sur le nombre d’arguments de cette derni`re. e e

59

le nombre n d’arguments effectifs, et interdit l’´criture de fonctions admettant un nombre variable d’arguments e (de telles fonctions sont possibles en C, songez ` printf et scanf). a

5.3

Exemples de production de code

Quand on n’est pas connaisseur de la question on peut croire que la g´n´ration de code machine est la partie e e la plus importante d’un compilateur, et donc que c’est elle qui d´termine la structure g´n´rale de ce dernier. e e e On est alors surpris, voire frustr´, en constatant la place consid´rable que les ouvrages sp´cialis´s consacrent ` e e e e a l’analyse (lexicale, syntaxique, s´mantique, etc.). C’est que62 : e La bonne mani`re d’´crire un compilateur du langage L pour la machine M consiste ` ´crire un analyseur e e ae du langage L auquel, dans un deuxi`me temps, on ajoute –sans rien lui enlever– les op´rations qui produisent e e du code pour la machine M . Dans cette section nous expliquons, ` travers des exemples, comment ajouter ` notre analyseur les op´rations a a e de g´n´ration de code qui en font un compilateur. Il faudra se souvenir de ceci : quand on r´fl´chit ` la g´n´ration e e e e a e e de code on est concern´ par deux ex´cutions diff´rentes : d’une part l’ex´cution du compilateur que nous r´alisons, e e e e e qui produit comme r´sultat un programme P en langage machine, d’autre part l’ex´cution du programme P ; a e e priori cette ex´cution a lieu ult´rieurement, mais nous devons l’imaginer en mˆme temps que nous ´crivons le e e e e compilateur, pour comprendre pourquoi nous faisons que ce dernier produise ce qu’il produit. 5.3.1 Expressions arithm´tiques e

Puisque la machine Mach 1 est une machine ` pile, la traduction des expressions arithm´tiques, aussi a e complexes soient-elles, est extrˆmement simple et ´l´gante. A titre d’exemple, imaginons que notre langage e ee source est une sorte de  C francis´ , et consid´rons la situation suivante (on suppose qu’il n’y a pas d’autres e e d´clarations de variables que celles qu’on voit ici) : e entier x, y, z; ... entier uneFonction() { entier a, b, c; ... y = 123 * x + c; } Pour commencer, int´ressons-nous ` l’expression 123 * x + c. Son analyse syntaxique aura ´t´ faite par e a ee des fonctions expArith (expression arithm´tique) et finExpArith ressemblant ` ceci : e a void expArith(void) { terme(); finExpArith(); } void finExpArith(void) { if (uniteCourante == ’+’ || uniteCourante == ’-’) { uniteCourante = uniteSuivante(); terme(); finExpArith(); } } (les fonctions terme et finTerme sont analogues aux deux pr´c´dentes, avec ‘∗’ et ‘/’ dans les rˆles de ‘+’ et e e o ‘−’, et facteur dans le rˆle de terme). Enfin, une version simplifi´e63 de la fonction facteur pourrait commencer o e comme ceci :
g´n´ralement : la bonne fa¸on d’obtenir l’information port´e par un texte soumis ` une syntaxe consiste ` ´crire l’analyseur e e c e a ae syntaxique correspondant et, dans un deuxi`me temps, ` lui ajouter les op´rations qui construisent les informations en question. e a e Ce principe est tout ` fait fondamental. Si vous ne deviez retenir qu’une seule chose de ce cours, que ce soit cela. a 63 Attention, nous envisageons momentan´ment un langage ultra-simple, sans tableaux ni appels de fonctions, dans lequel une e occurrence d’un identificateur dans une expression indique toujours ` une variable simple. a
62 Plus

60

void facteur(void) { if (uniteCourante == NOMBRE) uniteCourante = uniteSuivante(); else if (uniteCourante == IDENTIFICATEUR) uniteCourante = uniteSuivante(); else ... } Le principe de fonctionnement d’une machine ` pile, expliqu´ ` la section 5.2.1, a la cons´quence fondamentale a ea e suivante : 1. La compilation d’une expression produit une suite d’instructions du langage machine (plus ou moins longue, selon la complexit´ de l’expression) dont l’ex´cution a pour effet global d’ajouter une valeur au e e sommet de la pile, ` savoir le r´sultat de l’´valuation de l’expression. a e e 2. La compilation d’une instruction du langage source produit une suite d’instructions du langage machine (plus ou moins longue, selon la complexit´ de l’expression) dont l’ex´cution laisse la pile dans l’´tat o` e e e u elle se trouvait avant de commencer l’instruction en question. Une mani`re de retrouver quel doit ˆtre le code produit pour la compilation de l’expression 123 * x + c e e consiste ` se dire que 123 est d´j` une expression correcte ; l’effet d’une telle expression doit ˆtre de mettre au a ea e sommet de la pile la valeur 123. Par cons´quent, la compilation de l’expression 123 doit donner le code e EMPC 123 De mˆme, la compilation de l’expression x doit donner e EMPG EMPL 0 2 (car x est, dans notre exemple, la premi`re variable globale) et celle de l’expression c doit donner e (car c est la troisi`me variable locale). Pour obtenir ces codes il suffit de transformer la fonction facteur comme e 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 ´l´ment (un opcode ou un op´rande) dans le code. Si nous ee e supposons que ce dernier est rang´ dans la m´moire, ce pourrait ˆtre tout simplement : e e e void genCode(int element) { mem[TC++] = element; } Pour la production de code, les fonctions expArith et terme n’ont pas besoin d’ˆtre modifi´es. Seules les e e fonctions dans lesquelles des op´rateurs apparaissent explicitement doivent l’ˆtre : e e

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 final, le code produit ` l’occasion de la compilation de l’expression 123 * x + c sera donc : a EMPC 123 EMPG 0 MUL EMPL 2 ADD et, en imaginant l’ex´cution de la s´quence ci-dessus, on constate que son effet global aura bien ´t´ d’ajouter e e ee une valeur au sommet de la pile. Consid´rons maintenant l’instruction compl`te y = 123 * x + c. Dans un compilateur ultra-simplifi´ qui e e e ignorerait les appels de fonction et les tableaux, on peut imaginer qu’elle aura ´t´ analys´e par une fonction ee e instruction commen¸ant comme ceci : c void instruction(void) { if (uniteCourante == IDENTIFICATEUR) { uniteCourante = uniteSuivante(); terminal(’=’); expArith(); terminal(’;’); } else ... } /* instruction d’affectation */

pour qu’il produise du code il faut transformer cet analyseur comme ceci : void instruction(void) { if (uniteCourante == IDENTIFICATEUR) { /* instruction d’affectation */ 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 finalement produit par la compilation de y = 123 * x + c aura ´t´ : ee EMPC 123 EMPG 0 MUL EMPL 2 ADD DEPG 1 (y est la deuxi`me variable globale). Comme pr´vu, l’ex´cution du code pr´c´dent laisse la pile comme elle ´tait e e e e e e en commen¸ant. c 5.3.2 Instruction conditionnelle et boucles

La plupart des langages modernes poss`dent des instructions conditionnelles et des  boucles tant que  e d´finies par des productions analogues ` la suivante : e a instruction → ... | tantque expression faire instruction | si expression alors instruction | si expression alors instruction sinon instruction | ... La partie de l’analyseur syntaxique correspondant ` la r`gle pr´c´dente ressemblerait ` ceci64 : a e e e a 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¸ons par le code ` g´n´rer ` l’occasion d’une boucle  tant que . Dans ce code on trouvera les suites c a e e a d’instructions g´n´r´es pour l’expression et pour l’instruction que la syntaxe pr´voit (ces deux suites peuvent e ee e ˆtre tr`s longues, cela d´pend de la complexit´ de l’expression et de l’instruction en question). e e e e L’effet de cette instruction est connu : l’expression est ´valu´e ; si elle est fausse (c.-`-d. nulle) l’ex´cution e e a e continue apr`s le corps de la boucle ; si l’expression est vraie (c.-`-d. non nulle) le corps de la boucle est ex´cut´, e a e e puis on revient ` l’´valuation de l’expression et on recommence tout. a e Notons expr1 , expr2 , ... exprn les codes-machine produits par la compilation de l’expression et instr1 , instr2 , ... instrk ceux produits par la compilation de l’instruction. Pour l’instruction  tant que  toute enti`re on aura e donc un code comme ceci (les rep`res dans la colonne de gauche, comme α et β, repr´sentent les adresses des e e instructions) :
64 Notez que, dans le cas de l’instruction conditionnelle, l’utilisation d’un analyseur r´cursif descendant nous a permis de faire e une factorisation ` gauche originale du d´but commun des deux formes de l’instruction si (` propos de factorisation ` gauche voyez a e a a ´ventuellement la section 3.1.3). e

63

α

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

β Le point le plus difficile, ici, est de r´aliser qu’au moment o` le compilateur doit produire l’instruction e u SIFAUX β, l’adresse β n’est pas connue. Il faut donc produire quelque chose comme SIFAUX 0 et noter que la case dans laquelle est inscrit le 0 est ` corriger ult´rieurement (quand la valeur de β sera connue). Ce qui donne a e 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` repCode (pour  r´parer le code ) est une fonction aussi simple que genCode (cette fonction est tr`s simple u e e parce que nous supposons que notre compilateur produit du code dans la m´moire ; elle serait consid´rablement e e plus complexe si le code ´tait produit dans un fichier) : e void repCode(int place, int valeur) { mem[place] = valeur; } Instruction conditionnelle. Dans le mˆme ordre d’id´es, voici le code ` produire dans le cas de l’inse e a truction conditionnelle ` une branche ; il n’est pas difficile d’imaginer ce qu’il faut ajouter, et o`, dans l’analyseur a u montr´ plus haut pour lui faire produire le code suivant : e ... expr1 d´but de l’instruction si...alors... e expr2 ... exprn SIFAUX β instr1 instr2 ... instrk β ... la suite (apr`s l’instruction si...alors...) e 64

Et voici le code ` produire pour l’instruction conditionnelle ` deux branches. On note alors1 , alors2 , ... a a alorsk les codes-machine produits par la compilation de la premi`re branche et sinon1 , sinon2 , ... sinonm ceux e produits par la compilation de la deuxi`me branche : e expr1 expr2 ... exprn SIFAUX β alors1 alors2 ... alorsk SAUT γ sinon1 sinon2 ... sinonm ... d´but de l’instruction si...alors...sinon... e

d´but de la premi`re branche e e

β

d´but de la deuxi`me branche e e

γ 5.3.3

la suite (apr`s l’instruction si...alors...sinon...) e

Appel de fonction

Pour finir cette galerie d’exemples, examinons quel doit ˆtre le code produit ` l’occasion de l’appel d’une e a fonction, aussi bien du cˆt´ de la fonction appelante que de celui de la fonction appel´e. oe e Supposons que l’on a d’une part compil´ la fonction suivante : e entier distance(entier a, entier b) { entier x; x = a - b; si x < 0 alors x = - x; retour x; } et supposons que l’on a d’autre 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 l’affectation pr´c´dente : e e

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´sultat de la fonction e premier argument

ceci ach`ve le calcul du second argument e on vire les arguments (ensuite le r´sultat de la fonction est au sommet) e

ceci ach`ve le calcul du membre droit de l’affectation e

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

allocation de l’espace local (une variable)

debut de l’instruction si

] ] simulation du – unaire ]

β

le r´sultat de la fonction e

R´f´rences ee
[1] Alfred Aho, Ravi Sethi, and Jeffrey Ullman. Compilateurs. Principes, techniques et outils. Dunod, Paris, 1991. [2] Alfred Aho and Jeffrey 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. O’Reilly & Associates, Inc., 1990.

66

Tab. 1 – Les instructions de la machine Mach 1 opcode EMPC EMPL op´rande e valeur adresse explication EMPiler Constante. Empile la valeur indiqu´e. e EMPiler la valeur d’une variable Locale. Empile la valeur de la variable d´termin´e par le d´placement relatif ` BEL donn´ par adresse (entier relae e e a e tif). DEPiler dans une variable Locale. D´pile la valeur qui est au sommet et la e range dans la variable d´termin´e par le d´placement relatif ` BEL donn´ par e e e a e adresse (entier relatif). EMPiler la valeur d’une variable Globale. Empile la valeur de la variable d´termin´e par le d´placement (relatif ` BEG) donn´ par adresse. e e e a e DEPiler dans une variable Globale. D´pile la valeur qui est au sommet et la e range dans la variable d´termin´e par le d´placement (relatif ` BEG) donn´ e e e a e par adresse. EMPiler la valeur d’un ´l´ment de Tableau. D´pile la valeur qui est au sommet ee e de la pile, soit i cette valeur. Empile la valeur de la cellule qui se trouve i cases au-del` de la variable d´termin´e par le d´placement (relatif ` BEG) indiqu´ a e e e a e par adresse. DEPiler dans un ´l´ment de Tableau. D´pile une valeur v, puis une valeur i. ee e Ensuite range v dans la cellule qui se trouve i cases au-del` de la variable a d´termin´e par le d´placement (relatif ` BEG) indiqu´ par adresse. e e e a e ADDition. D´pile deux valeurs et empile le r´sultat de leur addition. e e SOUStraction. D´pile deux valeurs et empile le r´sultat de leur soustraction. e e MULtiplication. D´pile deux valeurs et empile le r´sultat de leur multiplication. e e DIVision. D´pile deux valeurs et empile le quotient de leur division euclidienne. e MODulo. D´pile deux valeurs et empile le reste de leur division euclidienne. e D´pile deux valeurs et empile 1 si elles sont ´gales, 0 sinon. e e INFerieur. D´pile deux valeurs et empile 1 si la premi`re est inf´rieure ` la e e e a seconde, 0 sinon. INFerieur ou EGal. D´pile deux valeurs et empile 1 si la premi`re est inf´rieure e e e ou ´gale ` la seconde, 0 sinon. e a D´pile une valeur et empile 1 si elle est nulle, 0 sinon. e Obtient de l’utilisateur un nombre et l’empile ECRIre Valeur. Extrait la valeur qui est au sommet de la pile et l’affiche Saut inconditionnel. L’ex´cution continue par l’instruction ayant l’adresse ine diqu´e. e Saut conditionnel. D´pile une valeur et si elle est non nulle, l’ex´cution contie e nue par l’instruction ayant l’adresse indiqu´e. Si la valeur d´pil´e est nulle, e e e l’ex´cution continue normalement. e Comme ci-dessus, en permutant nul et non nul. Appel de sous-programme. Empile l’adresse de l’instruction suivante, puis fait la mˆme chose que SAUT. e Retour de sous-programme. D´pile une valeur et continue l’ex´cution par l’inse e truction dont c’est l’adresse. Entr´e dans un sous-programme. Empile la valeur courante de BEL, puis copie e la valeur de SP dans BEL. Sortie d’un sous-programme. Copie la valeur de BEL dans SP, puis d´pile une e valeur et la range dans BEL. Allocation et restitution d’espace dans la pile. Ajoute nbreMots, qui est un entier positif ou n´gatif, ` SP e a La machine s’arrˆte. e

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