Vous êtes sur la page 1sur 67

Licence Universitaire Professionnelle

Gnie Logiciel & Thorie des Langages e e

Techniques et outils pour la compilation


Henri Garreta
Facult des Sciences de Luminy - Universit de la Mditerrane e e e e Janvier 2001

Table des mati`res e


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

3 Analyse syntaxique 3.1 Grammaires non contextuelles . . . . . . . . . . . . . . . . . . 3.1.1 Dnitions . . . . . . . . . . . . . . . . . . . . . . . . e 3.1.2 Drivations et arbres de drivation . . . . . . . . . . . e e 3.1.3 Qualits 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 rcursif . . . . . . . . . . . e 3.2.3 Analyse par descente rcursive . . . . . . . . . . . . . e 3.3 Analyseurs ascendants . . . . . . . . . . . . . . . . . . . . . . 3.3.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.2 Analyse LR(k) . . . . . . . . . . . . . . . . . . . . . . 3.4 Yacc, un gnrateur danalyseurs syntaxiques . . . . . . . . . e e 3.4.1 Structure dun chier source pour yacc . . . . . . . . . 3.4.2 Actions smantiques et valeurs des attributs . . . . . . e 3.4.3 Conits et ambigu es . . . . . . . . . . . . . . . . . . t

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

4 Analyse smantique e 4.1 Reprsentation et reconnaissance des types . . e 4.2 Dictionnaires (tables de symboles) . . . . . . . 4.2.1 Dictionnaire global & dictionnaire local 4.2.2 Tableau ` acc`s squentiel . . . . . . . . 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 dobjets . . . . . . . . . . . . . . . 5.1.2 Do` viennent les adresses des objets ? . . u 5.1.3 Compilation spare 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 gnrale de la machine Mach 1 e e 5.2.3 Jeu dinstructions . . . . . . . . . . . . . 5.2.4 Complments sur lappel des fonctions . . e 5.3 Exemples de production de code . . . . . . . . . 5.3.1 Expressions arithmtiques . . . . . . . . . e 5.3.2 Instruction conditionnelle et boucles . . . 5.3.3 Appel de fonction . . . . . . . . . . . . .

Introduction

Dans le sens le plus usuel du terme, la compilation est une transformation que lon fait subir ` un programme a crit dans un langage volu pour le rendre excutable. Fondamentalement, cest une traduction : un texte crit e e e e e en Pascal, C, Java, etc., exprime un algorithme et il sagit de produire un autre texte, spciant le mme e e algorithme dans le langage dune machine que nous cherchons ` programmer. a En gnralisant un peu, on peut dire que compiler cest lire une suite de caract`res obissant ` une certaine e e e e a syntaxe, en construisant une (autre) reprsentation de linformation que ces caract`res expriment. De ce point e e de vue, beaucoup doprations apparaissent comme tant de la compilation ; ` la limite, la lecture dun nombre, e e a quon obtient en C par une instruction comme : scanf("%f", &x); est dj` de la compilation, puisquil sagit de lire des caract`res constituant lcriture dune valeur selon la ea e e syntaxe des nombres dcimaux et de fabriquer une autre reprsentation de la mme information, ` savoir sa e e e a valeur numrique. e Bien sr, les questions qui nous intresseront ici seront plus complexes que la simple lecture dun nombre. u e Mais il faut comprendre que le domaine dapplication des principes et mthodes de lcriture de compilateurs e e contient bien dautres choses que la seule production de programmes excutables. Chaque fois que vous aurez e ae ` crire un programme lisant des expressions plus compliques que des nombres vous pourrez tirer prot des e concepts, techniques et outils expliqus dans ce cours. e

1.1

Structure de principe dun compilateur

La nature de ce qui sort dun compilateur est tr`s variable. Cela peut tre un programme excutable pour e e e un processeur physique, comme un Pentium III ou un G4, ou un chier de code pour une machine virtuelle, comme la machine Java, ou un code abstrait destin ` un outil qui en fera ultrieurement du code excutable, ea e e ou encore le codage dun arbre reprsentant la structure logique dun programme, etc. e En entre dun compilateur on trouve toujours la mme chose : une suite de caract`res, appele le texte e e e e source 1 . Voici les phases dans lesquelles se dcompose le travail dun compilateur, du moins dun point de vue e logique2 (voyez la gure 1) : Analyse lexicale Dans cette phase, les caract`res isols qui constituent le texte source sont regroups pour e e e former des units lexicales, qui sont les mots du langage. e Lanalyse lexicale op`re sous le contrle de lanalyse syntaxique ; elle appara comme une sorte de fonction e o t de  lecture amliore , qui fournit un mot lors de chaque appel. e e Analyse syntaxique Alors que lanalyse lexicale reconna les mots du langage, lanalyse syntaxique en ret conna les phrases. Le rle principal de cette phase est de dire si le texte source appartient au langage t o considr, cest-`-dire sil est correct relativement ` la grammaire de ce dernier. ee a a Analyse smantique La structure du texte source tant correcte, il sagit ici de vrier certaines proprits e e e ee smantiques, cest-`-dire relatives ` la signication de la phrase et de ses constituants : e a a les identicateurs apparaissant dans les expressions ont-ils t declars ? ee e les oprandes ont-ils les types requis par les oprateurs ? e e les oprandes sont-ils compatibles ? ny a-t-il pas des conversions ` insrer ? e a e les arguments des appels de fonctions ont-ils le nombre et le type requis ? etc. Gnration de code intermdiaire Apr`s les phases danalyse, certains compilateurs ne produisent pas die e e e rectement le code attendu en sortie, mais une reprsentation intermdiaire, une sorte de code pour une e e machine abstraite. Cela permet de concevoir indpendamment les premi`res phases du compilateur (constie e tuant ce que lon appelle sa face avant) qui ne dpendent que du langage source compil et les derni`res e e e phases (formant sa face arri`re) qui ne dpendent que du langage cible ; lidal serait davoir plusieurs e e e faces avant et plusieurs faces arri`re quon pourrait assembler librement3 . e
1 Conseil : le texte source a probablement t compos ` laide dun 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 lhabitude de limaginer comme sil tait crit sur un long et a e e mince ruban, formant une seule ligne. 2 Cest une organisation logique ; en pratique certaines de ces phases peuvent tre imbriques, et dautres 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 dinformaticien.

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

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

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

Fig. 1 Phases logiques de la compilation dune instruction

Optimisation du code Il sagit gnralement ici de transformer le code an que le programme rsultant e e e sexcute plus rapidement. Par exemple e dtecter linutilit de recalculer des expressions dont la valeur est dj` connue, e e ea a e e e transporter ` lextrieur des boucles des expressions et sous-expressions dont les oprandes ont la mme valeur ` toutes les itrations a e e dtecter, et supprimer, les expressions inutiles 4

etc. e e Gnration du code nal Cette phase, la plus impressionnante pour le nophyte, nest pas forcment la plus e e dicile ` raliser. Elle ncessite la connaissance de la machine cible (relle, virtuelle ou abstraite), et a e e e notamment de ses possibilits en mati`re de registres, piles, etc. e e

Analyse lexicale

Lanalyse lexicale est la premi`re phase de la compilation. Dans le texte source, qui se prsente comme un e e ot de caract`res, lanalyse lexicale reconna des units lexicales, qui sont les  mots  avec lesquels les phrases e t e sont formes, et les prsente ` la phase suivante, lanalyse syntaxique. e e a Les principales sortes dunits lexicales quon trouve dans les langages de programmation courants sont : e les caract`res spciaux simples : +, =, etc. e e les caract`res spciaux doubles : <=, ++, etc. e e les mots-cls : if, while, etc. e les constantes littrales : 123, -5, etc. e et les identicateurs : i, vitesse_du_vent, etc. A propos dune unit lexicale reconnue dans le texte source on doit distinguer quatre notions importantes : e lunit lexicale, reprsente gnralement 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 prcdents, les lex`mes e ne e e e e correspondants sont : "+", "=", "<=", "++", "if", "while", "123", "-5", "i" et "vitesse_du_vent" ventuellement, un attribut, qui dpend de lunit lexicale en question, et qui la compl`te. Seules les e e e e derni`res des dix units prcdentes ont un attribut ; pour un nombre, il sagit de sa valeur (123, 5) ; e e e e pour un identicateur, il sagit dun renvoi ` une table dans laquelle sont placs tous les identicateurs a e rencontrs (on verra cela plus loin). e le mod`le qui sert ` spcier lunit lexicale. Nous verrons ci-apr`s des moyens formels pour dnir 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 spciaux simples et doubles et les mots rservs, le lex`me et le mod`le co e e e e e e ncident, le mod`le dun nombre est  une suite de chires, ventuellement prcde dun signe , e e e e e e e c le mod`le dun identicateur est  une suite de lettres, de chires et du caract`re , commenant par une lettre . Outre la reconnaissance des units lexicales, les analyseurs lexicaux assurent certaines tches mineures comme e a la suppression des caract`res de dcoration (blancs, tabulations, ns de ligne, etc.) et celle des commentaires e e (gnralement considrs comme ayant la mme valeur quun blanc), linterface avec les fonctions de lecture de e e ee e caract`res, ` travers lesquelles le texte source est acquis, la gestion des chiers et lachage des erreurs, etc. e a Remarque. La fronti`re entre lanalyse lexicale et lanalyse syntaxique nest pas xe. Dailleurs, lanalyse e lexicale nest pas une obligation, on peut concevoir des compilateurs dans lesquels la syntaxe est dnie ` partir e a des caract`res individuels. Mais les analyseurs syntaxiques quil faut alors crire sont bien plus complexes que e e ceux quon obtient en utilisant des analyseurs lexicaux pour reconna les mots du langage. tre Simplicit et ecacit sont les raisons dtre des analyseurs lexicaux. Comme nous allons le voir, les teche e e niques pour reconna les units lexicales sont bien plus simples et ecaces que les techniques pour vrier la tre e e syntaxe.

2.1
2.1.1

Expressions rguli`res e e
Dnitions e

Les expressions rguli`res sont une importante notation pour spcier formellement des mod`les. Pour les e e e e dnir correctement il nous faut faire leort dapprendre un peu de vocabulaire nouveau : e Un alphabet est un ensemble de symboles. Exemples : {0, 1}, {A, C, G, T }, lensemble de toutes les lettres, lensemble des chires, le code ASCII, etc. On notera que les caract`res blancs (cest-`-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

tabulations et les marques de n de ligne) ne font gnralement pas partie des alphabets5 . e e Une cha (on dit aussi mot) sur un alphabet est une squence nie de symboles de . Exemples, ne e respectivement relatifs aux alphabets prcdents : 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 rduit ` lunique cha vide. Des exemples plus intressants (relatifs aux alphabets e a ne e prcdents) : lensemble des nombres en notation binaire, lensemble des cha e e nes ADN, lensemble des mots de la langue franaise, etc. c Si x et y sont deux cha nes, la concatnation de x et y, note xy, est la cha obtenue en crivant y e e ne e immdiatement apr`s x. Par exemple, la concatnation des cha e e e nes anti et moine est la cha antimoine. Si ne x est une cha on dnit x0 = et, pour n > 0, xn = xn1 x = xxn1 . On a donc x1 = x, x2 = xx, x3 = xxx, ne, e etc. Les oprations sur les langages suivantes nous serviront ` dnir les expressions rguli`res. Soient L et M e a e e e deux langages, on dnit : e dnomination e lunion de L et M la concatnation de L et M e la fermeture de Kleene de L la fermeture positive de L notation LM LM L L+ dnition 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 dnition de LM on dduit 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 chires. En considrant quun caract`re est la mme chose quune cha de longueur un, on peut e e e ne voir L et C comme des langages, forms de cha e nes de longueur un. Dans ces conditions : L C est lensemble des lettres et des chires, LC est lensemble des cha nes formes dune lettre suivie dun chire, e L4 est lensemble des cha nes de quatre lettres, L est lensemble des cha nes faites dun nombre quelconque de lettres ; en fait partie, C + est lensemble des cha nes de chires comportant au moins un chire, L(L C) est lensemble des cha nes de lettres et chires commenant par une lettre. c ` Expression reguliere. Soit un alphabet. Une expression rguli`re r sur est une formule qui dnit e e e un langage L(r) sur , de la mani`re suivante : e 1. est une expression rguli`re qui dnit le langage {} e e e 2. Si a , alors a est une expression rguli`re qui dnit le langage6 {a} e e e 3. Soient x et y deux expressions rguli`res, dnissant les langages L(x) et L(y). Alors e e e (x)|(y) est une expression rguli`re dnissant le langage L(x) L(y) e e e (x)(y) est une expression rguli`re dnissant le langage L(x)L(y) e e e (x) est une expression rguli`re dnissant le langage (L(x)) e e e (x) est une expression rguli`re dnissant le langage L(x) e e e La derni`re r`gle ci-dessus signie quon peut encadrer une expression rguli`re par des parenth`ses sans e e e e e changer le langage dni. Dautre part, les parenth`ses apparaissant dans les r`gles prcdentes peuvent souvent e e e e e tre omises, en fonction des oprateurs en prsence : il sut de savoir que les oprateurs , concatnation et | e e e e e sont associatifs ` gauche, et vrient a e priorit ( ) > priorit ( concatnation ) > priorit ( | ) e e e e Ainsi, on peut crire lexpression rguli`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 dcoule que les units lexicales, sauf mesures particuli`res (apostrophes, quillemets, etc.), ne peuvent pas contenir des e e e caract`res blancs. Dautre part, la plupart des langages autorisent les caract`res blancs entre les units lexicales. e e e 6 On prendra garde ` labus de langage quon fait ici, en employant la mme notation pour le caract`re a, la cha a e e ne a et lexpression rguli`re a. En principe, le contexte permet de savoir de quoi on parle. e e

` Definitions regulieres. Les expressions rguli`res se construisent ` partir dautres expressions rguli`res ; e e a e e cela am`ne ` des expressions passablement touues. On les all`ge en introduisant des dnitions rguli`res qui e a e e e e permettent de donner des noms ` certaines expressions en vue de leur rutilisation. 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 , . . . di1 , et chaque ri une u ne expression rguli`re sur {d1 , d2 , . . . di1 }. e e Exemple. Voici quelques dnitions rguli`res, et notamment celles de identicateur et nombre, qui dnissent e e e e les identicateurs et les nombres du langage Pascal : lettre A | B | . . . | Z | a | b | . . . | z chire 0 | 1 | . . . | 9 identicateur lettre ( lettre | chire ) chires chire chire fraction-opt . chires | exposant-opt ( E (+ | - | ) chires ) | nombre chires fraction-opt exposant-opt Notations abregees. Pour allger certaines critures, on compl`te la dnition des expressions rguli`res e e e e e e en ajoutant les notations suivantes : soit x une expression rguli`re, dnissant le langage L(x) ; alors (x)+ est une expression rguli`re, qui e e e e e dnit le langage (L(x))+ , e soit x une expression rguli`re, dnissant le langage L(x) ; alors (x)? est une expression rguli`re, qui e e e e e dnit le langage L(x) { }, e si c1 , c2 , . . . ck sont des caract`res, lexpressions rguli`re c1 |c2 | . . . |ck peut se noter [c1 c2 . . . ck ], e e e ` lintrieur dune paire de crochets comme ci-dessus, lexpression c1 c2 dsigne la squence de tous les a e e e caract`res c tels que c1 c c2 . e Les dnitions de lettre et chire donnes ci-dessus peuvent donc se rcrire : e e ee lettre [AZaz] chire [09] 2.1.2 Ce que les expressions rguli`res ne savent pas faire e e

Les expressions rguli`res sont un outil puissant et pratique pour dnir les units lexicales, cest-`-dire e e e e a les constituants lmentaires des programmes. Mais elles se prtent beaucoup moins bien ` la spcication de ee e a e constructions de niveau plus lev, car elles deviennent rapidement dune trop grande complexit. e e e De plus, on dmontre quil y a des cha e nes quon ne peut pas dcrire par des expressions rguli`res. Par e e e exemple, le langage suivant (suppos inni) e { a, (a), ((a)), (((a))), . . . } ne peut pas tre dni par des expressions rguli`res, car ces derni`res ne permettent pas dassurer quil 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 rguli`res  ne savent pas compter . e e Pour spcier ces structures quilibres, si importantes dans les langages de programmation (penser aux e e e parenth`ses dans les expressions arithmtiques, les crochets dans les tableaux, begin...end, {...}, if...then..., etc.) e e nous ferons appel aux grammaires non contextuelles, expliques ` la section 3.1. e a
7 On

assure souvent la sparation entre et les noms des dnitions rguli`res par des conventions typographiques. e e e e

2.2

Reconnaissance des units lexicales e

Nous avons vu comment spcier les units lexicales ; notre probl`me maintenant est dcrire un programme e e e e qui les reconna dans le texte source. Un tel programme sappelle un analyseur lexical. t Dans un compilateur, le principal client de lanalyseur lexical est lanalyseur syntaxique. Linterface entre ces deux analyseurs est une fonction int uniteSuivante(void) 8 , qui renvoie ` chaque appel lunit lexicale suivante a e trouve dans le texte source. e Cela suppose que lanalyseur lexical et lanalyseur syntaxique partagent les dnitions des constantes convene tionnelles dnissant les units lexicales. Si on programme en C, cela veut dire que dans les chiers sources des e e deux analyseurs on a inclus un chier dentte (chier  .h ) comportant une srie de dnitions comme9 : e e e #define #define #define #define #define etc. IDENTIF NOMBRE SI ALORS SINON 1 2 3 4 5

Cela suppose aussi que lanalyseur lexical et lanalyseur syntaxique partagent galement une variable globale e contenant le lex`me correspondant ` la derni`re unit lexicale reconnue, ainsi quune variable globale contenant e a e e le (ou les) attribut(s) de lunit lexicale courante, lorsque cela est pertinent, et notamment lorsque lunit lexicale e e est NOMBRE ou IDENTIF. On se donnera, du moins dans le cadre de ce cours, quelques  r`gles du jeu  supplmentaires : e e lanalyseur lexical est  glouton  : chaque lex`me est le plus long possible10 ; e e e e seul lanalyseur lexical acc`de au texte source. Lanalyseur syntaxique nacquiert ses donnes dentre autrement qu` travers la fonction uniteSuivante ; a lanalyseur lexical acquiert le texte source un caract`re ` la fois. Cela est un choix que nous faisons ici ; e a dautres choix auraient t possibles, mais nous verrons que les langages qui nous intressent 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 units lexicales INFEG, DIFF, INF, EGAL, SUPEG, SUP, IDENTIF, respectivement dnies par les expressions e e rguli`res <=, <>, <, =, >=, > et lettre(lettre|chire) , lettre et chire ayant leurs dnitions dj` vues. e e e ea Les diagrammes de transition sont une tape prparatoire pour la ralisation dun analyseur lexical. Au fur e e e et ` mesure quil reconna une unit lexicale, lanalyseur lexical passe par divers tats. Ces tats sont numrots a t e e e e e et reprsents dans le diagramme par des cercles. e e De chaque tat e sont issues une ou plusieurs `ches tiquetes par des caract`res. Une `che tiquete par e e e e e e e e c relie e ` ltat e1 dans lequel lanalyseur passera si, alors quil se trouve dans ltat e, le caract`re c est lu dans a e e e le texte source. Un tat particulier reprsente ltat initial de lanalyseur ; on le signale en en faisant lextrmit dune `che e e e e e e tiquete debut. e e Des doubles cercles identient les tats naux, correspondant ` la reconnaissance compl`te dune unit e a e e lexicale. Certains tats naux sont marqus dune toile : cela signie que la reconnaissance sest faite au prix e e e de la lecture dun caract`re au-del` de la n du lex`me11 . e a e Par exemple, la gure 2 montre les diagrammes traduisant la reconnaissance des units lexicales INFEG, e DIFF, INF, EGAL, SUPEG, SUP et IDENTIF. Un diagramme de transition est dit non dterministe lorsquil existe, issues dun mme tat, plusieurs `ches e e e e tiquetes par le mme caract`re, ou bien lorsquil existe des `ches tiquetes par la cha vide . Dans le cas e e e e e e e ne
on a employ loutil lex pour fabriquer lanalyser lexical, cette fonction sappelle plutt yylex ; le lex`me est alors point par e o e e la variable globale yytext et sa longueur est donne par la variable globale yylen. Tout cela est expliqu ` la section 2.3. e ea 9 Peu importent les valeurs numriques utilises, ce qui compte est quelles soient distinctes. e e 10 Cette r`gle est peu mentionne dans la littrature, pourtant elle est fondamentale. Cest grce ` elle que 123 est reconnu comme e e e a a un nombre, et non plusieurs, vitesseV ent comme un seul identicateur, et f orce comme un identicateur, et non pas comme un mot rserv suivi dun identicateur. e e 11 Il faut tre attentif ` ce caract`re, car il est susceptible de faire partie de lunit lexicale suivante, surtout sil nest pas blanc. e a e e
8 Si

dbut

<

= >
autre

2 3 4
*

rendre INFEG rendre DIFF rendre INF

= >

5 rendre EGAL 6 =
autre lettre

7 8
*

rendre SUPEG rendre SUP

lettre

9
chiffre

autre

10

* rendre IDENTIF

Fig. 2 Diagramme de transition pour les oprateurs de comparaison et les identicateurs e

contraire, le diagramme est dit dterministe. Il est clair que le diagramme de la gure 2 est dterministe. Seuls e e les diagrammes dterministes nous intresseront dans le cadre de ce cours. e e 2.2.2 Analyseurs lexicaux programms  en dur  e

Les diagrammes de transition sont une aide importante pour lcriture danalyseurs lexicaux. Par exemple, e a ` partir du diagramme de la gure 2 on peut obtenir rapidement un analyseur lexical reconnaissant les units e INFEG, DIFF, INF, EGAL, SUPEG, SUP et IDENTIF. Auparavant, nous apportons une lg`re modication ` nos diagrammes de transition, an de permettre que e e a les units lexicales soient spares par un ou plusieurs blancs12 . La gure 3 montre le (dbut du) diagramme e e e e modi13 . e
blanc dbut < = > lettre

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

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 n de ligne e que cela revient ` modier toutes les expressions rguli`res, en remplaant  <=  par  (blanc) <= ,  <  par  (blanc) < , a e e c

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 prcdent 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 lecacit de ces fonctions, au dtriment de leur scurit dutilisation, 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 dun 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 ny a jamais plus dun caract`re lu en trop). Dclaration : 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 rservs appartiennent au langage dni par lexpression e e e rguli`re lettre(lettre|chire) , tout comme les identicateurs. Leur reconnaissance peut donc se traiter de deux e e mani`res : e soit on incorpore les mots rservs au diagrammes de transition, ce qui permet dobtenir un analyseur e e tr`s ecace, mais au prix dun travail de programmation plus important, car les diagrammes de transition e deviennent tr`s volumineux14 , e soit on laisse lanalyseur traiter de la mme mani`re les mots rservs et les identicateurs puis, quand la e e e e reconnaissance dun  identicateur-ou-mot-rserv  est termine, on recherche le lex`me dans une table e e e e pour dterminer sil sagit dun identicateur ou dun mot rserv. e e e Dans les analyseurs lexicaux  en dur  on utilise souvent la deuxi`me mthode, plus facile ` programmer. e e a On se donne donc une table de mots rservs : e e struct { char *lexeme; int uniteLexicale; } motRes[] = { { "si", SI }, { "alors", ALORS }, { "sinon", SINON }, ... }; int nbMotRes = sizeof motRes / sizeof motRes[0]; puis on modie de la mani`re suivante la partie concernant les identicateurs de la fonction uniteSuivante : e ... else if (estLettre(c)) { lonLex = 0; lexeme[lonLex++] = c;
14 Nous

/* etat =

9 */

utiliserons cette solution quand nous tudierons loutil 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 nis

Un automate ni est dni par la donne de e e un ensemble ni dtats E, e un ensemble ni de symboles (ou alphabet) dentre , e une fonction de transition, transit : E E, un tat 0 distingu, appel tat initial, e e ee un ensemble dtats F , appels tats dacceptation ou tats naux. e e e e Un automate peut tre reprsent graphiquement par un graphe o` les tats sont gurs par des cercles (les e e e u e e tats naux par des cercles doubles) et la fonction de transition par des `ches tiquetes par des caract`res : e e e e e si transit(e1 , c) = e2 alors le graphe a une `che tiquete 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 gure 2). Si on en reparle ici cest quon peut en dduire un autre style danalyseur lexical, assez dirent de ce e e que nous avons appel analyseur programm  en dur . e e On dit quun automate ni accepte une cha dentre s = c1 c2 . . . ck si et seulement si il existe dans le ne e graphe de transition un chemin joignant ltat initial e0 ` un certain tat nal ek , compos de k `ches tiquetes e a e e e e e par les caract`res c1 , c2 , . . . ck . e Pour transformer un automate ni en un analyseur lexical il sura donc dassocier une unit lexicale ` e a chaque tat nal et de faire en sorte que lacceptation dune cha produise comme rsultat lunit lexicale e ne e e associe ` ltat nal en question. e a e Autrement dit, pour programmer un analyseur il sura maintenant dimplmenter la fonction transit ce e qui, puisquelle est dnie sur des ensembles nis, pourra se faire par une table ` double entre. Pour les e a e diagrammes des gures 2 et 3 cela donne la table suivante (les tats naux sont indiqus en gras, leurs lignes e e ont t supprimes) : 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 chire 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 lessentiel du travail de lanalyseur se rduira ` rpter  btement  laction etat = transit[etat][lireCar( e a e e e jusqu` tomber sur un tat nal. 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 dni par e e e final[e] = 0 si e nest pas un tat nal (vu comme un boolen, final[e] est faux), e e final[e] = U + 1 si e est nal, sans toile et associ ` lunit lexicale U (en tant que boolen, final[e] e ea e e est vrai, car les units lexicales sont numrotes au moins ` partir de zro), e e e a e final[e] = (U + 1) si e est nal, toil et associ ` lunit lexicale U (en tant que boolen, final[e] e e ea e e est encore vrai). Enn, voici comment les tableaux transit et final devraient tre initialiss pour correspondre aux diae e grammes des gures 2 et 315 : void initialiser(void) { int i, j; for (i = 0; i < NBR_ETATS; i++) final[i] = 0; final[ 2] = INFEG + 1; final[ 3] = DIFF + 1; final[ 4] = - (INF + 1); final[ 5] = EGAL + 1; final[ 7] = SUPEG + 1; final[ 8] = - (SUP + 1); final[10] = - (IDENTIF + 1); final[NBR_ETATS] = ERREUR + 1; for (i = 0; i < NBR_ETATS; i++) for (j = 0; j < NBR_CARS; j++) transit[i][j] = NBR_ETATS; transit[0][ ] = 0; transit[0][\t] = 0; transit[0][\n] = 0; transit[0][<] = 1; transit[0][=] = 5; transit[0][>] = 6; for (j = A; j <= Z; j++) transit[0][j] = 9; for (j = a; j <= z; j++) transit[0][j] = 9; for (j = 0; j < NBR_CARS; j++) transit[1][j] = 4; transit[1][=] = 2; transit[1][>] = 3;
15 Nous avons ajout un tat supplmentaire, ayant le numro NBR ETATS, qui correspond ` la mise en erreur de lanalyseur 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 gnrateur danalyseurs lexicaux e e

Les analyseurs lexicaux bass sur des tables de transitions sont les plus ecaces... une fois la table de e transition construite. Or, la construction de cette table est une opration longue et dlicate. e e Le programme lex 16 fait cette construction automatiquement : il prend en entre un ensemble dexpressions e rguli`res et produit en sortie le texte source dun programme C qui, une fois compil, est lanalyseur lexical e e e correspondant au langage dni par les expressions rguli`res en question. e e e Plus prcisment (voyez la gure 4), lex produit un chier source C, nomm lex.yy.c, contenant la dnition e e e e de la fonction int yytext(void), qui est lexacte homologue de notre fonction uniteSuivante de la section 2.2.2 : un programme appelle cette fonction et elle renvoie une unit lexicale reconnue dans le texte source. e

expressions rgulires

lex

lex.yy.c

lex.yy.c gcc autres modules monProg

source analyser

monProg

rsultat de l'analyse

Fig. 4 Utilisation de lex

2.3.1

Structure dun chier source pour lex

En lisant cette section, souvenez-vous de ceci : lex crit un chier source C. Ce chier est fait de trois sortes e dingrdients : e des tables garnies de valeurs calcules ` partir des expressions rguli`res fournies, e a e e a des morceaux de code C invariable, et notamment le  moteur  de lautomate, cest-`-dire la boucle qui rp`te inlassablement etat transit (etat, caractere), e e e e a des morceaux de code C, trouvs dans le chier source lex et recopis tels quels, ` lendroit voulu, dans le chier produit. Un chier source pour lex doit avoir un nom qui se termine par  .l . Il est fait de trois sections, dlimites e e par deux lignes rduites17 au symbole %% : e %{ dclarations pour le compilateur C e %} dnitions rguli`res e e e
est un programme gratuit quon trouve dans le syst`me UNIX pratiquement depuis ses dbuts. De nos jours on utilise e e souvent ex, une version amliore de lex qui appartient ` la famille GNU. e e a 17 Notez que les symboles %%, %{ et %}, quand ils apparaissent, sont crits au dbut de la ligne, aucun blanc ne les prc`de. e e e e
16 Lex

14

%% r`gles e %% fonctions C supplmentaires e La partie  dclarations pour le compilateur C  et les symboles %{ et %} qui lencadrent peuvent tre omis. e e Quand elle est prsente, cette partie se compose de dclarations qui seront simplement recopies au dbut du e e e e chier produit. En plus dautres choses, on trouve souvent ici une directive #include qui produit linclusion du chier  .h  contenant les dnitions des codes conventionnels des units lexicales (INFEG, INF, EGAL, etc.). e e La troisi`me section  fonctions C supplmentaires  peut tre absente galement (le symbole %% qui la spare 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 recopies ` la n du chier produit. e a ` Definitions regulieres. Les dnitions rguli`res sont de la forme e e e identicateur expressionRguli`re e e o` identicateur est crit au dbut de la ligne (pas de blancs avant) et spar de expressionRguli`re par des u e e e e e e blancs. Exemples : lettre chiffre [A-Za-z] [0-9]

Les identicateurs ainsi dnis peuvent tre utiliss dans les r`gles et dans les dnitions subsquentes ; il e e e e e e faut alors les encadrer par des accolades. Exemples : lettre chiffre alphanum %% {lettre}{alphanum}* {chiffre}+("."{chiffre}+)? expressionRguli`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` expressionRguli`re est crit au dbut 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 expressionRguli`re e e { action } signie  ` la n de la reconnaissance dune cha du langage dni par expressionRguli`re excutez action . a ne e e e e Le traitement par lex dune telle r`gle consiste donc ` recopier laction indique ` 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 signication e est claire : quand une cha du texte source est reconnue, la fonction yylex se termine en rendant comme ne rsultat lunit lexicale reconnue. Il faudra appeler de nouveau cette fonction pour que lanalyse du texte source e e reprenne. A la n de la reconnaissance dune unit lexicale la cha accepte est la valeur de la variable yytext, de e ne e type cha de caract`res19 . Un caract`re nul indique la n 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 copies dans une fonction quon a le droit dy utiliser linstruction return. e variable yytext est dclare dans le chier produit par lex ; il vaut mieux ne pas chercher ` y faire rfrence dans dautres e e a ee chiers, car il nest pas spci laquelle des dclarations  extern char *yytext  ou  extern char yytext[]  est pertinente. e e e
19 La 18 Cest

15

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

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

` Expressions regulieres. Les expressions rguli`res acceptes par lex sont une extension de celles dnies e e e e a ` la section 2.1. Les mta-caract`res prcdemment introduits, cest-`-dire (, ), |, , +, ?, [, ] et ` lintrieur e e e e a a e des crochets, sont lgitimes dans lex et y ont le mme sens. En outre, on dispose de ceci (liste non exhaustive) : e e un point . signie un caract`re quelconque, dirent de la marque de n de ligne, e e on peut encadrer par des guillemets un caract`re ou une cha pour viter que les mta-caract`res qui sy e ne, e e e trouvent soient interprts comme tels. Par exemple, "." signie le caract`re . (et non pas un caract`re ee e e quelconque), " " signie un blanc, "[a-z]" signie la cha [a-z], etc., ne Dautre part, on peut sans inconvnient encadrer par des guillemets un caract`re ou une cha qui nen e e ne avaient pas besoin, lexpression [^caract`res] signie : tout caract`re nappartenant pas ` lensemble dni par [caract`res], e e a e e lexpression  ^expressionRguli`re  signie : toute cha reconnue par expressionRguli`re ` la condition e e ne e e a quelle soit au dbut dune ligne, e lexpression  expressionRguli`re$  signie : toute cha reconnue par expressionRguli`re ` la condition e e ne e e a quelle soit ` la n dune ligne. a Attention. Il faut tre tr`s soigneux en crivant les dnitions et les r`gles dans le chier source lex. En e e e e e eet, tout texte qui nest pas exactement ` sa place (par exemple une dnition ou une r`gle qui ne commencent a e e pas au dbut de la ligne) sera recopi dans le chier produit par lex. Cest un comportement voulu, parfois utile, e e mais qui peut conduire ` des situations confuses. a Echo du texte analyse. Lanalyseur lexical produit par lex prend son texte source sur lentre standard20 e et lcrit, avec certaines modications, sur la sortie standard. Plus prcisement : e e tous les caract`res qui ne font partie daucune cha reconnue sont recopis sur la sortie standard (ils e ne e  traversent  lanalyseur lexical sans en tre aects), e e une cha accepte au titre dune expression rguli`re nest pas recopie sur la sortie standard. ne e e e e Bien entendu, pour avoir les cha nes acceptes dans le texte crit par lanalyseur il sut de le prvoir dans e e e laction correspondante. Par exemple, la r`gle suivante reconna les identicateurs et fait en sorte quils gurent 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 frquemment dans les actions. On peut labrger en ECHO : t e e e

Voici le texte source pour crer lanalyseur lexical dun langage comportant les nombres et les identicateurs e dnis comme dhabitude, les mots rservs si, alors, sinon, tantque, faire et rendre et les oprateurs e e e e doubles ==, !=, <= et >=. Les units lexicales correspondantes sont respectivement reprsentes par les constantes e e e conventionnelles IDENTIF, NOMBRE, SI, ALORS, SINON, TANTQUE, FAIRE, RENDRE, EGAL, DIFF, INFEG, SUPEG, dnies dans le chier unitesLexicales.h. e Pour les oprateurs simples on dcide que tout caract`re non reconnu par une autre expression rguli`re est e e e e e une unit lexicale, et quelle est reprsente par son propre code ASCII21 . Pour viter des collisions entre ces e e e e codes ASCII et les units lexicales nommes, on donne ` ces derni`res des valeurs suprieures ` 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 dfaut 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 quon sen remet au  client  de lanalyseur lexical, cest-`-dire lanalyseur syntaxique, pour sparer les oprateurs a e e prvus par le langage des caract`res spciaux qui nen sont pas. Ou, dit autrement, que nous transformons lerreur  caract`re e e e e illgal , ` 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 dmonstratif) : 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 lanalyseur : si x == 123 alors y = 0; Cration dun excutable et essai sur le texte prcdent (rappelons que lex sappelle ex 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 chier source pour lex est appele lorsque lanalyseur t e rencontre la n du chier ` analyser22 . Outre dventuelles actions utiles dans telle ou telle application para e ticuli`re, cette fonction doit rendre une valeur non nulle pour indiquer que le ot dentre est dnitivement e e e puis, ou bien ouvrir un autre ot dentre. e e e 2.3.3 Autres utilisations de lex

Ce que le programme gnr par lex fait ncessairement, cest reconna e ee e tre les cha nes du langage dni e par les expressions rguli`res donnes. Quand une telle reconnaissance est accomplie, on nest pas oblig de e e e e renvoyer une unit lexicale pour signaler la chose ; on peut notamment dclencher laction quon veut et ne pas e e retourner ` la fonction appelante. Cela permet dutiliser lex pour eectuer dautres 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 prcdent exploite le fait que tous les caract`res qui ne font pas partie dune cha reconnue e e e ne sont recopis sur la sortie ; ainsi, la plupart des caract`res du texte donn seront recopis tels quels. Les cha e e e e nes reconnues sont dnies par lexpression rguli`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, rduite ` { return 1 ; }, est fournie et on na pas ` e a a sen occuper.

18

une suite dau moins un chire, ventuellement, un point suivi dun nombre quelconque de chires, e ventuellement, un nombre quelconque de blancs (espaces, tabulations, ns de ligne), e un F majuscule obligatoire, ventuellement, la cha rancs ou un point (ainsi,  F ,  F.  et  Francs  sont tous trois accepts), e ne e enn, un blanc obligatoire. Lorsquune cha saccordant ` cette syntaxe est reconnue, comme  99.50 Francs , la fonction atof ne a obtient la valeur que [le dbut de] cette cha reprsente. Il sut alors de mettre dans le texte de sortie le e ne e rsultat de la division de cette valeur par le taux adquat ; soit, ici,  15.17 EUR . e e

3
3.1

Analyse syntaxique
Grammaires non contextuelles

Les langages de programmation sont souvent dnis par des r`gles rcursives, 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 Dnitions 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 dpart 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 lusage que nous en faisons dans le cadre de lcriture de compilateurs, nous pouvons e expliquer ces lments de la mani`re suivante : ee e 1. Les symboles terminaux sont les symboles lmentaires qui constituent les cha ee nes du langage, les phrases. Ce sont donc les units lexicales, extraites du texte source par lanalyseur lexical (il faut se rappeler que e lanalyseur syntaxique ne conna pas les caract`res dont le texte source est fait, il ne voit ce dernier que t e comme une suite dunits lexicales). e 2. Les symboles non terminaux sont des variables syntaxiques dsignant des ensembles de cha e nes de symboles terminaux. e e 3. Le symbole de dpart est un symbole non terminal particulier qui dsigne le langage en son entier. 4. Les productions peuvent tre interprtes de deux mani`res : e ee e comme des r`gles dcriture (on dit plutt de rcriture), permettant dengendrer 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 dun S2 [de toutes les mani`res possibles] suivi dun . . . suivi dun Sk [de toutes les mani`res possibles] , e e e comme des r`gles danalyse, on dit aussi reconnaissance. La production S S1 S2 . . . Sk se lit alors  pour reconna un S, dans une suite de terminaux donne, il faut reconna un S1 suivi dun S2 tre e tre suivi dun . . . suivi dun Sk  La dnition dune grammaire devrait donc commencer par lnumration 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 lensemble de tous les symboles terminaux apparaissant dans les productions, VN est lensemble de tous les symboles non terminaux apparaissant dans les productions, le symbole de dpart est le membre gauche de la premi`re production. e e En outre, on all`ge les notations en dcidant que si plusieurs productions ont le mme 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 reprsente un symbole non terminal, ne e une cha en caract`res tltype ou "entre guillemets", reprsente un symbole terminal. ne e e e e A titre dexemple, voici la grammaire G1 dnissant le langage dont les cha e nes sont les expressions arithmtiques formes avec des nombres, des identicateurs et les deux oprateurs + et *, comme  60 * vitesse e e e + 200 . Suivant notre convention, les symboles non terminaux sont expression, terme et facteur ; le symbole de dpart est expression : e expression expression "+" terme | terme terme terme "*" facteur | facteur facteur nombre | identificateur | "(" expression ")" 3.1.2 Drivations et arbres de drivation e e (G1 )

Derivation. Le processus par lequel une grammaire dnit un langage sappelle drivation. 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 quil existe dans P une production A . Quelles que soient les suites de symboles et , on dit que A se drive en une tape en la suite ce qui scrit e e e A Cette dnition justie la dnomination grammaire non contextuelle (on dit aussi grammaire indpendante e e e du contexte ou context free). En eet, dans la suite A les cha nes et sont le contexte du symbole A. Ce que cette dnition dit, cest que le symbole A se rcrit 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 drive en n en n tapes, et on crit e e e 0 n . Enn, si se drive en en un nombre quelconque, ventuellement nul, dtapes on dit simplement que e e e se drive en et on crit e e . Soit G = {VT , VN , S0 , P } une grammaire non contextuelle ; le langage engendr par G est lensemble des e cha nes de symboles terminaux qui drivent de S0 : e L (G) = w VT | S0 w Si w L(G) on dit que w est une phrase de G. Plus gnralement, 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 considrons la cha "60 * vitesse + 200" qui, une fois lue par lanalyseur lexical, se prsente ainsi : e ne e w = ( nombre "*" identificateur "+" nombre ). Nous avons expression w, cest ` dire w L(G1 ) ; en a eet, nous pouvons exhiber la suite de drivations 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 drivation prcdente est appele une drivation gauche car elle est enti`rement e e e e e e compose de drivations en une tape dans lesquelles ` chaque fois cest le non-terminal le plus ` gauche qui est e e e a a rcrit. On peut dnir de mme une drivation droite, o` ` chaque tape cest le non-terminal le plus ` droite ee e e e ua e a qui est rcrit. ee Arbre de derivation. Soit w une cha de symboles terminaux du langage L(G) ; il existe donc une ne drivation telle que S0 w. Cette drivation peut tre reprsente graphiquement par un arbre, appel arbre e e e e e e de drivation, dni de la mani`re suivante : e e e

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


Fig. 5 Arbre de drivation e la racine de larbre est le symbole de dpart, e les nuds intrieurs sont tiquets par des symboles non terminaux, e e e si un nud intrieur e est tiquet par le symbole S et si la production S S1 S2 . . . Sk a t utilise pour e e e ee e driver S alors les ls de e sont des nuds tiquets, de la gauche vers la droite, par S1 , S2 . . . Sk , e e e les feuilles sont tiquetes par des symboles terminaux et, si on allonge verticalement les branches de e e larbre (sans les croiser) de telle mani`re que les feuilles soient toutes ` la mme hauteur, alors, lues de la e a e gauche vers la droite, elles constituent la cha w. ne Par exemple, la gure 5 montre larbre de drivation reprsentant la drivation donne en exemple ci-dessus. e e e e On notera que lordre des drivations (gauche, droite) ne se voit pas sur larbre. e 3.1.3 Qualits des grammaires en vue des analyseurs e

terme facteur facteur

"+" +

nombre 200

Etant donne une grammaire G = {VT , VN , S0 , P }, faire lanalyse syntaxique dune cha w VT cest e ne rpondre ` la question  w appartient-elle au langage L(G) ? . Parlant strictement, un analyseur syntaxique est e a donc un programme qui nextrait aucune information de la cha analyse, il ne fait quaccepter (par dfaut) ne e e ou rejeter (en annonant une erreurs de syntaxe) cette cha c ne. En ralit on ne peut pas empcher les analyseurs den faire un peu plus car, pour prouver que w L(G) e e e il faut exhiber une drivation S0 w, cest-`-dire construire un arbre de drivation dont la liste des feuilles e a e est w. Or, cet arbre de derivation est dj` une premi`re information extraite de la cha source, un dbut de ea e ne e  comprhension  de ce que le texte signie. e 21

Nous examinons ici des qualits quune grammaire doit avoir et des dfauts dont elle doit tre exempte pour e e e que la construction de larbre de drivation de toute cha du langage soit possible et utile. e ne Grammaires ambigues. Une grammaire est ambigu sil existe plusieurs drivations gauches direntes e e e pour une mme 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 eet, la gure 6 montre deux arbres de drivation distincts pour la cha "2 * 3 + 10". Ils correspondent e ne aux deux drivations 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

nombre nombre * 3 + 10

Fig. 6 Deux arbres de drivation pour la mme cha e e ne Deux grammaires sont dites quivalentes si elles engendrent le mme langage. Il est souvent possible de e e remplacer une grammaire ambigu par une grammaire non ambigu quivalente, mais il ny a pas une mthode e ee e gnrale 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 rcursive ` gauche sil existe un non-terminal A e a et une drivation de la forme A A, o` est une cha quelconque. Cas particulier, on dit quon a une e u ne rcursivit ` gauche simple si la grammaire poss`de une production de la forme A A. e ea e La rcursivit ` gauche ne rend pas une grammaire ambigu, mais empche lcriture danalyseurs 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 rcursive ` gauche, et mme simplement : e a e expression expression "+" terme | terme terme terme "*" facteur | facteur facteur nombre | identificateur | "(" expression ")" (G1 )

Il existe une mthode pour obtenir une grammaire non rcursive ` gauche quivalente ` une grammaire e e a e a donne. Dans le cas de la rcursivit ` gauche simple, cela consiste ` remplacer une production telle que e e ea a
24 La question est un peu prmature ici, mais nous verrons quun analyseur descendant est un programme compos de fonctions e e e directement dduites des productions. Une production A . . . donne lieu ` une fonction reconnaitre A dont le corps est fait e a dappels aux fonctions reconnaissant les symboles du membre droit de la production. Dans le cas dune production A A on se retrouve donc avec une fonction reconnaitre A qui commence par un appel de reconnaitre A. Bonjour la rcursion innie... e

22

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

(G3 )

A propos des -productions. La transformation de grammaire montre 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 lcriture dun analyseur, puisquune production telle que e e n expression "+" terme n expression | impliquera notamment que  une mani`re de reconna une n expression consiste ` ne rien reconna , ce e tre a tre qui est possible quelle que soit la cha dentre ; ainsi, notre grammaire semble devenir ambigu. On rsout ce ne e e e probl`me en imposant aux analyseurs que nous crirons la r`gle de comportement suivante : dans la drivation e e e e dun non-terminal, une -production ne peut tre choisie que lorsquaucune autre production nest applicable. e Dans lexemple prcdent, cela donne : si la cha dentre commence par + alors on doit ncessairement e e ne e e choisir la premi`re production. e ` Factorisation a gauche. Nous cherchons ` crire des analyseurs prdictifs. Cela veut dire qu` tout a e e a moment le choix entre productions qui ont le mme membre gauche doit pouvoir se faire, sans risque derreur, e en comparant le symbole courant de la cha ` analyser avec les symboles susceptibles de commencer les ne a drivations des membres droits des productions en comptition. e e Une grammaire contenant des productions comme A 1 | 2 viole ce principe car lorsquil faut choisir entre les productions A 1 et A 2 le symbole courant est un de ceux qui peuvent commencer une drivation de , et on ne peut pas choisir ` coup sr entre 1 et 2 . e a u Une transformation simple, appele factorisation ` gauche, corrige ce dfaut (si les symboles susceptibles de e a e commencer une rcriture de 1 sont distincts de ceux pouvant commencer une rcriture de 2 ) : ee ee A A A 1 | 2 Exemple classique. Les grammaires de la plupart des langages de programmation dnissent ainsi linstruce tion conditionnelle : instr si si expr alors instr | si expr alors instr sinon instr Pour avoir un analyseur prdictif il faudra oprer une factorisation ` gauche : e e a instr si si expr alors instr n instr si n instr si sinon instr | Comme prcdemment, lapparition dune -production semble rendre ambigu la grammaire. Plus prcisment, e e e e e la question suivante se pose : ny a-t-il pas deux arbres de drivation possibles pour la cha 26 : e ne si alors si alors sinon Nous lavons dj` 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 nest applicable. Autrement dit, si, au moment o` lanalyseur doit driver le nonu e ne e terminal n instr si, la cha dentre commence par le terminal sinon, alors la production  n instr si sinon instr  doit tre applique. La gure 7 montre larbre de drivation obtenu pour la cha prcdente. e e e ne e e
25 Pour se convaincre de lquivalence de ces deux grammaires il sut de sapercevoir que, si et sont des symboles terminaux, e alors elles engendrent toutes deux le langage {, , , , . . .} 26 Autre formulation de la mme 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 drivation 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 rguli`res, mais il existe des langages (pratiquement tous les langages de programmation, excusez du peu... !) e e quelles ne peuvent pas dcrire compl`tement. e e On dmontre par exemple que le langage L = wcw | w (a|b) , o` a, b et c sont des terminaux, ne peut e u pas tre dcrit par une grammaire non contextuelle. L est fait de phrases comportant deux cha e e nes de a et b identiques, spares par un c, comme ababcabab. Limportance de cet exemple provient du fait que L modlise e e e lobligation, quont la plupart des langages, de vrier que les identicateurs apparaissant dans les instructions e ont bien t pralablement dclars (la premi`re occurrence de w dans wcw correspond ` la dclaration dun ee e e e e a e identicateur, la deuxi`me occurrence de w ` lutilisation de ce dernier). e a Autrement dit, lanalyse syntaxique ne permet pas de vrier que les identicateurs utiliss dans les proe e grammes font lobjet de dclarations pralables. Ce probl`me doit ncessairement tre remis ` une phase e e e e e a ultrieure danalyse smantique. e e

3.2

Analyseurs descendants

Etant donne une grammaire G = (VT , VN , S0 , P ), analyser une cha de symboles terminaux w VT cest e ne construire un arbre de drivation prouvant que S0 w. e Les grammaires des langages que nous cherchons ` analyser ont un ensemble de proprits quon rsume en a ee e disant que ce sont des grammaires LL(1). Cela signie quon peut en crire des analyseurs : e lisant la cha source de la gauche vers la droite (gauche = left, cest le premier L), ne cherchant ` construire une drivation gauche (cest 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 cest ncessaire, une production parmi plusieurs candidates (cest le 1 de LL(1)). e A propos du symbole accesible. Pour rchir au fonctionnement de nos analyseurs il est utile dimaginer e e que la cha source est crite sur un ruban dlant derri`re une fentre, de telle mani`re quun seul symbole ne e e e e e est visible ` la fois ; voyez la gure 8. Un mcanisme permet de faire avancer jamais reculer le ruban, pour a e rendre visible le symbole suivant.

if
chane source dj examine
unit courante

avancer

chane source restant examiner

Fig. 8 Fentre ` symboles terminaux e a

24

Lorsque nous programmerons eectivement des analyseurs, cette  machine ` symboles terminaux  ne sera a rien dautre que lanalyseur lexical pralablement crit ; le symbole visible ` la fentre sera reprsent par une e e a e e e variable uniteCourante, et lopration  faire avancer le ruban  se traduira par uniteCourante = uniteSuivante() e (ou bien, si cest lex qui a crit lanalyseur lexical, uniteCourante = yylex()). e 3.2.1 Principe

Analyseur descendant. Un analyseur descendant construit larbre de drivation de la racine (le symbole e de dpart de la grammaire) vers les feuilles (la cha de terminaux). e ne Pour en dcrire schmatiquement le fonctionnement nous nous donnons une fentre ` symboles terminaux e e e a comme ci-dessus et une pile de symboles, cest-`-dire une squence de symboles terminaux et non terminaux a e a ` laquelle on ajoute et on enl`ve des symboles par une mme extrmit, en loccurrence lextrmit de gauche e e e e e e (cest une pile couche ` lhorizontale, qui se remplit de la droite vers la gauche). e a Initialisation. Au dpart, la pile contient le symbole de dpart de la grammaire et la fentre montre le e e e premier symbole terminal de la cha dentre. ne e Iteration. Tant que la pile nest pas vide, rpter les oprations 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 fentre est le mme symbole , alors a e e dpiler le symbole au sommet de la pile et e faire avancer le terminal visible ` la fentre, a e sinon, signaler une erreur (par exemple acher  attendu ) ; si le symbole au sommet de la pile est un non terminal S e sil y a une seule production S S1 S2 . . . Sk ayant S pour membre gauche alors dpiler S et empiler S1 S2 . . . Sk ` la place, a sil y a plusieurs productions ayant S pour membre gauche, alors dapr`s le terminal e visible ` la fentre, sans faire avancer ce dernier, choisir lunique production S a e S1 S2 . . . Sk pouvant convenir, dpiler S et empiler S1 S2 . . . Sk . e Terminaison. Lorsque la pile est vide si le terminal visible ` la fentre est la marque qui indique la n de la cha dentre alors lanalyse a e ne e a russi : la cha appartient au langage engendr par la grammaire, e ne e sinon, signaler une erreur (par exemple, acher  caract`res inattendus ` la suite dun texte e a correct ). A titre dexemple, voici la reconnaissance par un tel analyseur du texte "60 * vitesse + 200" avec la grammaire G3 de la section 3.1.3 : expression terme n expression n expression "+" terme n expression | terme facteur n terme n terme "*" facteur n terme | facteur nombre | identificateur | "(" expression ")"

(G3 )

La cha dentre est donc (nombre "*" identif "+" nombre). Les tats successifs de la pile et de la ne e e fentre sont les suivants : e

25

fentre e nombre nombre nombre nombre "*" "*" identificateur identificateur "+" "+" "+" "+" nombre nombre nombre

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

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

"+" terme terme facteur nombre

Lorsque la pile est vide, la fentre exhibe , la marque de n de cha e ne. La cha donne appartient donc ne e bien au langage considr. ee 3.2.2 Analyseur descendant non rcursif e

Nous ne dvelopperons pas cette voie ici, mais nous pouvons remarquer quon peut raliser des programmes e e itratifs qui implantent lalgorithme expliqu ` la section prcdente. e ea e e La plus grosse dicult est le choix dune production chaque fois quil faut driver un non terminal qui e e est le membre gauche de plusieurs productions de la grammaire. Comme ce choix ne dpend que du terminal e visible ` la fentre, on peut le faire, et de mani`re tr`s ecace, ` laide dune table ` double entre, appele table a e e e a a e e danalyse, calcule ` lavance, reprsentant une fonction Choix : VN VT P qui ` un couple (S, ) form e a e a e dun non terminal (le sommet de la pile) et un terminal (le symbole visible ` la fentre) associe la production a e quil faut utiliser pour driver S. e Pour avoir un analyseur descendant non rcursif il sut alors de se donner une fentre ` symboles terminaux e e a (cest-`-dire un analyseur lexical), une pile de symboles comme expliqu ` la section prcdente, une table a e a e e danalyse comme expliqu ici et un petit programme qui implante lalgorithme de la section prcdente, dans e e e lequel la partie  choisir la production...  se rsume ` une consultation de la table P = Choix(S, ). e a En dnitive, un analyseur descendant est donc un couple form dune table dont les valeurs sont intimement e e lies ` la grammaire analyse et dun programme tout ` fait indpendant de cette grammaire. e a e a e 3.2.3 Analyse par descente rcursive e

A loppos du prcdent, un analyseur par descente rcursive est un type danalyseur descendant dans lequel e e e e le programme de lanalyseur est troitement li ` la grammaire analyse. Voici les principes de lcriture dun e ea e e tel analyseur : e a 1. Chaque groupe de productions ayant le mme membre gauche S donne lieu ` une fonction void reconnaitre S(void), ou plus simplement void S(void). Le corps de cette fonction se dduit des membres droits e des productions en question, comme expliqu ci-apr`s. e e e 2. Lorsque plusieurs productions ont le mme membre gauche, le corps de la fonction correspondante est une conditionnelle (instruction if ) ou un aiguillage (instruction switch) qui, dapr`s le symbole terminal e visible ` la fentre, slectionne lexcution des actions correspondant au membre droit de la production a e e e pertinente. Dans cette slection, le symbole visible ` la fentre nest pas modi. e a e e 3. Une squence de symboles S1 S2 . . . Sn dans le membre droit dune production donne lieu, dans la fonction e correspondante, ` une squence dinstructions traduisant les actions  reconnaissance de S1 ,  recona e naissance de S2 , . . . reconnaissance de Sn . 26

4. Si S est un symbole non terminal, laction  reconnaissance de S  se rduit ` lappel de fonction recone a naitre S(). 5. Si est un symbole terminal, laction  reconnaissance de  consiste ` considrer le symbole terminal a e visible ` la fentre et a e sil est gal ` , faire passer la fentre sur le symbole suivant27 , e a e sinon, annoncer une erreur (par exemple, acher  attendu ). Lensemble des fonctions crites selon les prescriptions prcdentes forme lanalyseur du langage considr. e e e ee Linitialisation de lanalyseur consiste ` positionner la fentre sur le premier terminal de la cha dentre. a e ne e On lance lanalyse en appellant la fonction associe au symbole de dpart de la grammaire. Au retour de cette e e fonction si la fentre ` terminaux montre la marque de n de cha lanalyse a russi, e a ne, e sinon la cha est errone28 (on peut par exemple acher le message  caract`res illgaux apr`s une ne e e e e expression correcte ). Exemple. Voici encore la grammaire G3 de la section 3.1.3 : expression terme n expression n expression "+" terme n expression | terme facteur n terme n terme "*" facteur n terme | facteur nombre | identificateur | "(" expression ")" et voici lanalyseur par descente rcursive 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 cest ici le seul endroit, dans cette description, o` il est indiqu de faire avancer la fentre. u e e autre attitude possible -on la vue adopte par certains compilateurs de Pascal- consiste ` ne rien vrier au retour de e a e la fonction associe au symbole de dpart. Cela revient ` considrer 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 dun terminal revient frquemment dans un analyseur. Nous en avons fait une fonction e spare (on suppose que erreur est une fonction qui ache 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 prcdent en appliquant systmatiquement les r`gles donnes plus e e e e e e haut, obtenant ainsi un analyseur correct dont la structure re`te la grammaire donne. Mais il nest pas e e interdit de pratiquer ensuite certaines simplications, ne serait-ce pour rattraper certaines maladresses de notre dmarche. Lappel gnralis de la fonction terminal, notamment, est ` lorigine de certains test redondants. e e e e a Par exemple, la fonction n expression commence par les deux lignes if (uniteCourante == +) { terminal(+); ... si on dveloppe lappel de terminal, la maladresse de la chose devient vidente e e if (uniteCourante == +) { if (uniteCourante == +) lireUnite(); ... Une version plus raisonnable des fonctions n expression et facteur serait donc : void fin_expression(void) { if (uniteCourante == +) { lireUnite(); terme(); fin_expression(); } else /* rien */; } ... void facteur(void) { if (uniteCourante == NOMBRE) lireUnite(); else if (uniteCourante == IDENTIFICATEUR) lireUnite(); else { 28

terminal((); expression(); terminal()); } } Comme elle est 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 lavons dit, tant donnes une grammaire G = {VT , VN , S0 , P } et une cha w VT , le e e ne but de lanalyse syntaxique est la construction dun arbre de drivation qui prouve w L(G). Les mthodes e e descendantes construisent cet arbre en partant du symbole de dpart de la grammaire et en  allant vers  la e cha de terminaux. Les mthodes ascendantes, au contraire, partent des terminaux qui constituent la cha ne e ne dentre et  vont vers  le symbole de dpart. e e Le principe gnral des mthodes ascendantes est de maintenir une pile de symboles29 dans laquelle sont e e e empils (lempilement sappelle ici dcalage) les terminaux au fur et ` mesure quils sont lus, tant que les e e a symboles au sommet de la pile ne sont pas le membre droit dune production de la grammaire. Si les k symboles du sommet de la pile constituent le membre droit dune production alors ils peuvent tre dpils et remplacs e e e e par le membre gauche de cette production (cette opration sappelle rduction). Lorsque dans la pile il ny a e e plus que le symbole de dpart de la grammaire, si tous les symboles de la cha dentre ont t lus, lanalyse e ne e ee a russi. e Le probl`me majeur de ces mthodes 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 eectuer la rduction ? e lorsque les symboles au sommet de la pile sont le membre droit dune ou plusieurs productions, faut-il rduire tout de suite, ou bien continuer ` dcaler, an de permettre ultrieurement une rduction plus e a e e e juste ? A titre dexemple, avec la grammaire G1 de la section 3.1.1 : expression expression "+" terme | terme terme terme "*" facteur | facteur facteur nombre | identificateur | "(" expression ")" (G1 )

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

29

fentre e nombre "*" "*" "*" identificateur "+" "+" "+" "+" nombre

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

identificateur facteur

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

action dcalage e rduction e rduction e dcalage e dcalage e rduction e rduction e rduction e dcalage e dcalage e rduction e rduction e rduction e succ`s e

On dit que les mthodes de ce type eectuent une analyse par dcalage-rduction. Comme le montre le e e e tableau ci-dessus, le point important est le choix entre rduction et dcalage, chaque fois quune rduction est e e e possible. Le principe est : les rductions pratiques ralisent la construction inverse dune drivation droite. e e e e Par exemple, le rductions faites dans lanalyse prcdente construisent, ` lenvers, la drivation 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 ecaces que les analyseurs e descendants, et acceptant une classe de langages plus large que la classe des langages traits par ces derniers. e Le principal inconvnient de ces analyseurs est quils ncessitent des tables quil est extrmement dicile 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 prsente yacc, un des plus connus de ces outils. e Les analyseurs LR(k) lisent la cha dentre de la gauche vers la droite (do` le L), en construisant ne e u linverse dune drivation droite (do` le R) avec une vue sur la cha dentre large de k symboles ; lorsquon e u ne e dit simplement LR on sous-entend k = 1, cest le cas le plus frquent. e Etant donne une grammaire G = (VT , VN , S0 , P ), un analyseur LR est constitu par la donne dun ensemble e e e dtats E, dune fentre ` symboles terminaux (cest-`-dire un analyseur lexical), dune pile de doublets (s, e) e e a a o` s E et e VT et de deux tables Action et Suivant, qui reprsentent des fonctions : u e Action : E VT ({ decaler } E) ({reduire} P ) { succes, erreur } Suivant : E VN E Un analyseur LR comporte enn un programme, indpendant du langage analys, qui excute les oprations e e e e suivantes : Initialisation. Placer la fentre sur le premier symbole de la cha dentre et vider la pile. e ne e Itration. Tant que cest possible, rpter : e e e soit s ltat au sommet de la pile et le terminal visible ` la fentre e a e si Action(s, ) = (decaler, s ) empiler (, s ) placer la fentre sur le prochain symbole de la cha dentree e ne

30

sinon, si Action(s, ) = (reduire, A ) dpiler autant dlments de la pile quil 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 arrt e sinon erreur. Note. En ralit, notre pile est redondante. Les tats de lanalyseur reprsentent les diverses congurations e e e e dans lesquelles la pile peut se trouver, il ny a donc pas besoin dempiler les symboles, les tats susent. Nous e avons utilis une pile de couples (etat, symbole) pour clarier lexplication. e

3.4

Yacc, un gnrateur danalyseurs syntaxiques e e

Comme nous lavons indiqu, les tables Action et Suivant dun analyseur LR sont diciles ` construire e a manuellement, mais il existe des outils pour les dduire automatiquement des productions de la grammaire e considre. ee Le programme yacc 30 est un tel gnrateur danalyseurs syntaxiques. Il prend en entre un chier source e e e constitu essentiellement des productions dune grammaure non contextuelle G et sort ` titre de rsultat 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

rsultat de l'analyse

Fig. 9 Utilisation courante de yacc Dans la description de la grammaire donne ` yacc on peut associer des actions smantiques aux productions ; e a e ce sont des bouts de code source C que yacc place aux bons endroits de lanalyseur construit. Ce dernier peut ainsi excuter des actions ou produire des informations dduites du texte source, cest-`-dire devenir un compilateur. e e a Un analyseur syntaxique requiert pour travailler un analyseur lexical qui lui dlivre le ot dentre sous forme e e dunits lexicales successives. Par dfaut, yacc suppose que lanalyseur lexical disponible a t fabriqu par lex. e e ee e Autrement dit, sans quil faille de dclaration spciale pour cela, le programme produit par yacc comporte des e e appels de la fonction yylex aux endroits o` lacquisition dune unite lexicale est ncessaire. u e 3.4.1 Structure dun chier source pour yacc

Un chier source pour yacc doit avoir un nom termin par  .y . Il est fait de trois sections, dlimites par e e e deux lignes rduites au symbole %% : e %{ dclarations pour le compilateur C e %}
30 Dans le monde Linux on trouve une version amliore de yacc, nomme 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

dclarations pour yacc e %% r`gles (productions + actions smantiques) e e %% fonctions C supplmentaires e Les parties  dclarations pour le compilateur C  et  fonctions C supplmentaires  sont simplement e e recopies dans le chier produit, respectivement au dbut et ` la n de ce chier. Chacune de ces deux parties e e a peut tre absente. e Dans la partie  dclarations pour yacc  on rencontre souvent les dclarations des units lexicales, sous une e e e forme qui laisse yacc se charger dinventer des valeurs conventionnelles : %token NOMBRE IDENTIF VARIABLE TABLEAU FONCTION %token OU ET EGAL DIFF INFEG SUPEG %token SI ALORS SINON TANTQUE FAIRE RETOUR Ces dclarations dunits lexicales intressent yacc, qui les utilise, mais aussi lex, qui les manipule en tant que e e e rsultats de la fonction yylex. Pour cette raison, yacc produit31 un chier supplmentaire, nomm y.tab.h32 , e e e destin ` tre inclus dans le source lex (au lieu du chier que nous avons appel unitesLexicales.h dans e a e e lexemple de la section 2.3.2). Par exemple, le chier produit pour les dclarations 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 rle dunit 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 numrotes ` partir de ee e e a 256. Specification de VT , VN et S0 . Dans un chier source yacc : les caract`res simples, encadrs par des apostrophes comme dans les programmes C, et les identicateurs e e mentionns dans les dclarations %token sont tenus pour des symboles terminaux, e e tous les autres identicateurs apparaissant dans les productions sont consid`res comme des symboles non e terminaux, par dfaut, le symbole de dpart 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 mme membre e e gauche, chacune associ ` une action smantique. ea e Une action smantique (cf. section 3.4.2) est un morceau de code source C encadr par des accolades. Cest e e un code que lanalyseur excutera lorsque la production correspondante aura t utilise dans une rduction. Si e ee e e on crit un analyseur  pur , cest-`-dire un analyseur qui ne fait quaccepter ou rejeter la cha dentre, alors e a ne e il ny a pas dactions smantiques et les r`gles de traduction sont simplement les productions de la grammaire. e e Dans les r`gles de traduction, le mta-symbole est indiqu par deux points  :  et chaque r`gle (ceste e e e a `-dire chaque groupe de productions avec le mme membre gauche) est termine par un point-virgule  ; . La e e barre verticale  |  a la mme signication 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 chier source yacc (pour obtenir un analyseur pur, sans actions smantiques) : 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 loption -d, comme dans  yacc -d maSyntaxe.y . le cas de bison les noms des chiers  .tab.c  et  .tab.h  re`tent le nom du chier source  .y . e

32

Lanalyseur syntaxique se prsente comme une fonction int yyparse(void), qui rend 0 lorsque la cha e ne dentre est accepte, une valeur non nulle dans le cas contraire. Pour avoir un analyseur syntaxique autonome e e il sut donc dajouter, en troisi`me section du chier prcdent : e e e %% int main(void) { if (yyparse() == 0) printf("Texte correct\n"); } En ralit, il faut aussi crire la fonction appele en cas derreur. Cest une fonction de prototype void e e e e yyerror(char *message), elle est appele par lanalyseur avec un message derreur (par dfaut  parse error , e e ce nest pas tr`s informatif !). Par exemple : e void yyerror(char *message) { printf(" <<< %s\n", message); } N.B. Leet prcis de linstruction ci-dessus, cest-`-dire la prsentation eective des messages derreur, e a e dpend de la mani`re dont lanalyseur lexical crit les units lexicales au fur et ` mesure de lanalyse. e e e e a 3.4.2 Actions smantiques et valeurs des attributs e

Une action smantique est une squence dinstructions C crite, entre accolades, ` droite dune production. e e e a Cette squence est recopie par yacc dans lanalyseur produit, de telle mani`re quelle sera excute, pendant e e e e e lanalyse, lorsque la production correspondante aura t employe pour faire une rduction (cf.  analyse par ee e e dcalage-rduction  ` la section 3.3). e e a Voici un exemple simple, mais complet. Le programme suivant lit une expression arithmtique inxe33 e e forme de nombres, didenticateurs et des oprateurs + et , et crit la reprsentation en notation postxe34 e e e e e de la mme expression. e Fichier lexique.l : %{ #include "syntaxe.tab.h" extern char nom[]; /* cha^ne de caract`res partage avec lanalyseur 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 partage avec lanalyseur lexical */ e e { { { { } yylval = atoi(yytext); return nombre; } strcpy(nom, yytext); return identificateur; } return yytext[0]; }

33 Dans la notation inxe, un oprateur binaire est crit entre ses deux oprandes. Cest la notation habituelle, et elle est ambigu ; e e e e e cest pourquoi on lui associe un syst`me dassociativit et de priorit des oprateurs, et la possibilit dutiliser des parenth`ses. e e e e e e 34 Dans la notation postixe, appele aussi notation polonaise inverse, un oprateur binaire est crit ` la suite de ses deux e e e e a oprandes ; cette notation na besoin ni de priorits des oprateurs ni de parenth`ses, et elle nest 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 caractrisation du symbole. Par exemple, dans les langages qui nous intressent, la reconnaissance du lex`me e e e "2001" donne lieu ` lunit lexicale NOMBRE avec lattribut 2001. a e Un analyseur lexical produit par lex transmet les attributs des units lexicales ` un analyseur syntaxique e a produit par yacc ` travers une variable yylval qui, par dfaut35 , est de type int. Si vous allez voir le chier a e  .tab.h  fabriqu par yacc et destin ` tre inclus dans lanalyseur lexical, vous y trouverez, outre les dnitions e eae e des codes des units lexicales, les dclarations : e e #define YYSTYPE int ... extern YYSTYPE yylval; Nous avons dit que les actions smantiques sont des bouts de code C que yacc se borne ` recopier dans e a lanalyseur produit. Ce nest pas tout ` fait exact, dans les actions smantiques on peut mettre certains symboles a e bizarres, que yacc remplace par des expressions C correctes. Ainsi, $1, $2, $3, etc. dsignent les valeurs des e attributs des symboles constituant le membre droit de la production concerne, tandis que $$ dsigne la valeur e e de lattribut du symbole qui est le membre gauche de cette production.
35 Un mcanisme puissant et relativement simple permet davoir des attributs polymorphes, pouvant prendre plusieurs types e distincts. Nous ne ltudierons pas dans le cadre de ce cours. e

34

Laction smantique { $$ = $1 ; } est implicite et il ny a pas besoin de lcrire. e e Par exemple, voici notre analyseur prcdent, modi (lg`rement) pour en faire un calculateur de bureau e e e e e eectuant les quatre oprations lmentaires sur des nombres entiers, avec gestion des parenth`ses et de la e ee e priorit des oprateurs : e e Fichier lexique.l : le mme que prcdemment, ` ceci pr`s que les identicateurs ne sont plus reconnus. e e e a e Fichier syntaxe.y : %{ void yyerror(char *); %} %token nombre %% session

expression

terme

facteur

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

session expression =

{ printf("rsultat : %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 dutilisation ; reprsente la touche  n de chier , qui dpend du syst`me utilis (Ctrl-D, pour e e e e UNIX) : $ go 2 + 3 = rsultat : 5 e (2 + 3)*(1002 - 1 - 1) = rsultat : 5000 e Au revoir! 3.4.3 Conits et ambigu es t

Voici encore une grammaire quivalente ` la prcdente, mais plus compacte : e a e e ... %% session

: session expression = | ;

{ printf("rsultat: %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 conits. Lorsquil a e rencontre un conit36 , yacc applique une r`gle de rsolution par dfaut et continue son travail ; ` la n de ce e e e a dernier, il indique le nombre total de conits rencontrs et arbitrairement rsolus. Il est impratif de comprendre e e e la cause de ces conits et il est fortement recommand dessayer de les supprimer (par exemple en transformant e la grammaire). Les conits possibles sont : 1. Dcaler ou rduire ? ( shift/reduce conict ). Ce conit se produit lorsque lalgorithme de yacc narrive e e pas ` choisir entre dcaler et rduire, car les deux actions sont possibles et nam`nent pas lanalyse ` une a e e e a impasse. Un exemple typique de ce conit a pour origine la grammaire usuelle de linstruction conditionnelle instr si si expr alors instr | si expr alors instr sinon instr Le conit appara pendant lanalyse dune 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 rduire ces symboles en instr si (ce qui revient ` associer la partie sinon au premier si) ou bien faut-il e a dcaler (ce qui provoquera plus tard une rduction revenant ` associer la partie sinon au second si) ? e e a Rsolution par dfaut : lanalyseur fait le dcalage (cest un comportement  glouton  : chacun cherche ` e e e a manger le plus de terminaux possibles). 2. Comment rduire ? ( reduce/reduce conict ) Ce conit se produit lorsque lalgorithme ne peut pas e choisir entre deux productions distinctes dont les membres droits permettent tous deux de rduire les symboles e au sommet de la pile. On trouve un exemple typique dun tel conit 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 dtails, il e e e est facile dimaginer quon trouvera dans de telles grammaires des productions compl`tement direntes avec e e les mmes membres droits. Par exemple, la production dnissant un appel de fonction et celle dnissant un e e e acc`s ` un tableau pourraient ressembler ` ceci : e a a ... appel de fonction identicateur ( liste expressions ) ... acces tableau identicateur ( liste expressions ) ... La rsolution par dfaut est : dans lordre o` les r`gles sont crites dans le chier source pour yacc, on prf`re e e u e e ee la premi`re production. Comme lexemple ci-dessus le montre37 , cela ne rsout pas souvent bien le probl`me. e e e Grammaires doperateurs. La grammaire prcdente, ou du moins sa partie utile, la r`gle expression, e e e vrie ceci : e aucune r`gle ne contient d-production, e aucune r`gle ne contient une production ayant deux non-terminaux conscutifs. e e
36 Noubliez pas que yacc ne fait pas une analyse, mais un analyseur. Ce quil dtecte en ralit nest pas un conit, mais la e e e possibilit que lanalyseur produit en ait ultrieurement, du moins sur certains textes sources. e e 37 Le probl`me consistant ` choisir assez tt entre appel de fonction et acc`s ` un tableau, lorsque les notations sont les mmes, e a o e a e est souvent rsolu par des considrations smantiques : lidenticateur qui prc`de la parenth`se est cens avoir t dclar, on e e e e e e e e e e e consulte donc la table de symboles pour savoir si cest un nom de procdure ou bien un nom de tableau. Du point de vue de la e puissance des analyseurs syntaxiques, cest donc plutt un aveu dimpuissance... o

36

De telles grammaires sappellent des grammaires doprateurs. Nous nen ferons pas ltude dtaille ici, e e e e mais il se trouve quil est tr`s simple de les rendre non ambigus : il sut de prciser par ailleurs le sens de e e e lassociativit et la priorit de chaque oprateur. e e e En yacc, cela se fait par des dclarations %left et %right qui spcient le sens dassociativit des oprateurs, e e e e lordre de ces dclarations donnant leur priorit : ` chaque nouvelle dclaration les oprateurs dclars sont plus e e a e e e e prioritaires que les prcdents. e e Ainsi, la grammaire prcdente, prsente comme ci-apr`s, nest plus ambigu. On a ajout des dclarations e e e e e e e e indiquant que +, , et / sont associatifs ` gauche38 et que la priorit de et / est suprieure ` celle de + et a e e a . %{ void yyerror(char *); %} %token nombre %left + - %left * / %% session

expression

: | ; : | | | | | ;

session expression =

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

expression + expression - expression * expression / ( expression nombre

expression expression expression expression )

{ { { { {

$$ $$ $$ $$ $$

= = = = =

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

} } } }

%% ...

Analyse smantique e

Apr`s lanalyse lexicale et lanalyse syntaxique, ltape suivante dans la conception dun compilateur est e e lanalyse smantique dont la partie la plus visible est le contrle de type. Des exemples de tches lies au e o a e contrle de type sont : o construire et mmoriser des reprsentations des types dnis par lutilisateur, lorsque le langage le permet, e e e traiter les dclarations de variables et fonctions et mmoriser les types qui leur sont appliqus, e e e vrier que toute variable rfrence et toute fonction appele ont bien t pralablement dclares, e ee e e ee e e e vrier que les param`tres des fonctions ont les types requis, e e contrler les types des oprandes des oprations arithmtiques et en dduire le type du rsultat, o e e e e e au besoin, insrer dans les expressions les conversion imposes par certaines r`gles de compatibilit, e e e e etc. Pour xer les ides, voici une situation typique o` le contrle de type joue. Imaginons quun programme, e u o crit en C, contient linstruction  i = (200 + j) * 3.14 . Lanalyseur syntaxique construit un arbre abstrait e reprsentant cette expression, comme ceci (pour tre tout ` fait corrects, ` la place de i et j nous aurions d e e a a u reprsenter des renvois ` la table des symboles) : e a
38 Dire quun oprateur est associatif ` gauche [resp. ` droite] cest dire que a b c se lit (a b) c [resp. a (b c)]. La e a a question est importante, mme pour des oprateurs 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 prcis39 , tandis que les nuds e internes reprsentent des oprations abstraites, dont le type exact reste ` prciser. Le travail smantique ` faire e e a e e a consiste `  remonter les types , depuis les feuilles vers la racine, rendant concrets les oprateurs et donnant a e un type prcis aux sous-arbres. e Supposons par exemple que i et j aient t dclares de type entier. Lanalyse smantique de larbre prcdent ee e e e e e permet den dduire, successivement : e que le + est laddition des entiers, puisque les deux oprandes sont entiers, et donc que le sous-arbre e chapeaut par le + reprsente une valeur de type entier, e e que le est la multiplication des ottants, puisquun oprande est ottant40 , quil y a lieu de convertir e lautre oprande vers le type ottant, et que le sous-arbre chapeaut par reprsente un objet de type e e e ottant, que laectation qui coie larbre tout entier consiste donc en laectation dun ottant ` un entier, et a quil faut donc insrer une opration de troncation ottant entier ; en C, il en dcoule que larbre tout e e e entier reprsente une valeur du type entier. e En dnitive, le contrle de type transforme larbre prcdent en quelque chose qui ressemble ` ceci : e o e e a aectation dentiers rr r r i ottantentier multiplication ottante rr r entier ottant 3.14 addition enti`re e r r 200 j

4.1

Reprsentation et reconnaissance des types e

Une partie importante du travail smantique quun compilateur fait sur un programme est e pendant la compilation des dclarations, construire des reprsentations des types dclars dans le proe e e e gramme, pendant la compilation des instructions, reconna les types des objets intervenant dans les expressions. tre La principale dicult de ce travail est la complexit des structures ` construire et ` manipuler. En eet, e e a a dans les langages modernes les types sont dnis par des procds rcursifs quon peut composer ` volont. Par e e e e a e exemple, en C on peut avoir des entiers, des adresses (ou pointeurs) dentiers, des fonctions rendant des adresses dentiers, des adresses de fonctions rendant des adresses dentiers, des tableaux dadresses de fonctions rendant des adresses dentiers, etc. Cela peut aller aussi loin quon veut, lauteur dun compilateur doit se donner le moyen de reprsenter ces structures de complexit quelconque. e e Faute de temps, nous ntudierons pas cet aspect des compilateurs dans le cadre de ce cours. e
39 Le type dune constante est donn par une convention lexicale (exemple : 200 reprsente un entier, 3.14 un ottant), le type e e dune variable ou le type rendu par une fonction est spci par la dclaration de la variable ou la fonction en question. e e e 40 Cela sappelle la  r`gle du plus fort , elle est suivie par la plupart des langages : lorsque les oprandes dune opration e e e arithmtique ne sont pas de mme type, celui dont le type est le  plus fort  (plus complexe, plus prcis, plus tendu, etc.) tire ` e e e e a lui lautre oprande, provoquant une conversion de type. e

38

Ainsi, le compilateur que nous raliserons ` titre de projet ne traitera que les types entier et tableau dentiers. e a Pour les curieux, voici nanmoins une suggestion de structures de donnes pour la reprsentation des prine e e cipaux types du langage C dans un compilateur qui serait lui-mme41 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 reprsent par une structure ` deux champs : classe, dont la valeur est un code e e a conventionnel qui indique de quelle sorte de type il sagit, et attributs, qui donne les informations ncessaires e pour achever de dnir le type. Attributs est un champ polymorphe (en C, une union) dont la structure dpend e e de la valeur du champ classe : si la classe est celle dun type primitif, le champ attributs est sans objet,
une question troublante qui nit par appara tre dans tous les cours de compilation : peut-on crire le compilateur dun e langage en utilisant le langage quil sagit de compiler ? Malgr lapparent paradoxe, la chose est tout ` fait possible. Il faut e a comprendre que la question nest pas de savoir, par exemple, dans quel langage fut crit le tout premier compilateur de C, si tant e est quil y eut un jour un compilateur de C alors que la veille il ny en avait pas cela est le probl`me, peu intressant, de luf e e et de la poule. La question est plutt de savoir si, de nos jours, on peut utiliser un compilateur (existant) de C pour raliser un o e compilateur (nouveau) de C. Prsente 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, puisquil est lui-mme crit en C ? Le rsultat obtenu devra tre un nouvel a e e e e excutable identique ` celui du compilateur. On voit l` un des intrts quil y a ` crire le compilateur dun langage dans le langage e a a e e ae ` compiler : on dispose ipso facto dun formidable test de validation : au terme dun certain processus de construction (on dit a plutt bootstrap) on doit possder un compilateur C capable de compiler son propre texte source S et de donner un rsultat C(S) o e e vriant C(S) = C. e
41 Voici

39

si le champ classe indique quil sagit dun type pointeur, alors le champ attributs pointe sur la description du type des objets points, e e si la valeur du champ classe est tTableau alors il faut deux attributs pour dnir le type : nombreElements, le nombre de cases du tableau, et typeElement, qui pointe sur la description du type commun des lments ee du tableau, sil sagit dun type structure, lattribut est ladresse dune liste cha ee dont chaque maillon contient un n pointeur42 sur un descripteur de type, enn, si le champ classe indique quil sagit dun type fonction, alors le champ attribut se compose de ladresse dun descripteur de type (le type rendu par la fonction) et ladresse dune liste de types (les types des arguments de cette derni`re). e On facilite lallocation dynamique de telles structures en se donnant une fonction : descripteurType *nouveau(typePossible classe) { descripteurType *res = malloc(sizeof(descripteurType)); assert(res != NULL); res->classe = classe; return res; } Pour montrer le fonctionnement de cette structure de donnes, voici un exemple purement dmonstratif, la e e construction  ` la main  du descripteur correspondant au type de la variable dclare, en C, comme suit : a e e struct { char *lexeme; int uniteLexicale; } motRes[N]; La variable est motRes (nous lavons utilise ` la section 2.2.2), elle est dclare comme un tableau de N lments 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 n de la construction, par la variable tmp2) : e a ... listeDescripteursTypes *pCour, *pSuiv; descripteurType *pTmp1, *pTmp2; /* maillon de liste dcrivant le type entier */ e pCour = malloc(sizeof(listeDescripteursTypes)); pCour->info = nouveau(tInt); pCour->suiv = NULL; /* maillon de liste dcrivant 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 dun pointeur e e n sur un tel descripteur. Apparemment plus lourde ` grer, la solution adopte ici se rv`le ` lusage la plus souple. a e e e e a

40

Dans le mme ordre dides, voici la construction manuelle du descripteur du type  matrice de N L N C e e ottants  ou, plus prcisment,  tableau de N L lments qui sont des tableaux de N C ottants  (en C, cela e e ee scrit : float matrice[NL][NC] ;). A la n de la construction le descripteur cherch est point par pTmp2 : e e e ... /* description dune ligne, tableau de NC flottants: */ pTmp1 = nouveau(tTableau); pTmp1->attributs.casTableau.nombreElements = NC; pTmp1->attributs.casTableau.typeElement = nouveau(tFloat); /* description dune matrice, tableau de NL lignes: */ pTmp2 = nouveau(tTableau); pTmp2->attributs.casTableau.nombreElements = NL; pTmp2->attributs.casTableau.typeElement = pTmp1; ... Enn, pour donner encore un exemple de manipulation de ces structures de donnes, voici un utilitaire e fondamental dans les syst`mes de contrle de type : la fonction boolenne qui fournit la rponse ` la question e o e e a  deux descripteurs donns dcrivent-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 dclares avant e e e dtre utilises dans les instructions. Quel que soit le degr de complexit des types supports par notre come e e e e pilateur, celui-ci devra grer une table de symboles, appele aussi dictionnaire, dans laquelle se trouveront les e e identicateurs couramment dclars, chacun associ ` certains attributs, comme son type, son adresse 43 et e e e a dautres informations, cf. gure 10.

identif

type
tEntier

adresse complemt
180

v i t e s s e \0

Fig. 10 Une entre dans le dictionnaire e Nous tudions pour commencer le cahier des charges du dictionnaire, cest-`-dire les services que le compie a lateur en attend, puis, dans les sections suivantes, diverses implmentations possibles. e Grosso modo le dictionnaire fonctionne ainsi : quand le compilateur trouve un identicateur dans une dclaration, il le cherche dans le dictionnaire en e esprant ne pas le trouver (sinon cest lerreur  identicateur dj` dclar ), puis il lajoute au dictionnaire e ea e e avec le type que la dclaration spcie, e e quand le compilateur trouve un identicateur dans la partie excutable44 dun programme, il le cherche e dans le dictionnaire avec lespoir de le trouver (sinon cest lerreur  identicateur non dclar ), ensuite e e il utilise les informations que le dictionnaire associe ` lidenticateur. a Nous allons voir que la question est un peu plus complique que cela. e 4.2.1 Dictionnaire global & dictionnaire local

Dans les langages qui nous intressent, un programme est essentiellement une collection de fonctions, e entre lesquelles se trouvent des dclarations de variables. A lintrieur des fonctions se trouvent galement e e e des dclarations de variables. e Les variables dclares entre les fonctions et les fonctions elles-mmes sont des objets globaux. Un objet global e e e est visible45 depuis sa dclaration jusqu` la n du texte source, sauf aux endroits o` un objet local de mme e a u e nom le masque, voir ci-apr`s. e Les variables dclares ` lintrieur des fonctions sont des objets locaux. Un objet local est visible dans la e e a e fonction o` il est dclar, depuis sa dclaration jusqu` la n de cette fonction ; il nest 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 mme nom. e En dnitive, quand le compilateur se trouve dans47 une fonction il faut possder deux dictionnaires : un e e dictionnaire global, contenant les noms des objets globaux couramment dclars, et un dictionnaire local dans e e lequel se trouvent les noms des objets locaux couramment dclars (qui, parfois, masquent des objets dont les e e noms se trouvant dans le dictionnaire global). Dans ces conditions, lutilisation des dictionnaires que fait le compilateur se prcise : e
question des adresses des objets qui se trouvent dans les programmes sera tudie en dtail ` la section 5.1.1. e e e a les langages comme C, Java, etc., la partie excutable des programmes est lensemble des corps des fonctions dont le e programme se compose. En Pascal il faut ajouter ` cela le corps du programme. a 45 On dit quun objet o ayant le nom n est visible en un point dun programme si une occurrence de n en ce point est comprise comme dsignant o. Cela ne prjuge en rien de la correction ou de la lgalit de lemploi de n en ce point. e e e e 46 Par masquage dun objet o par un objet o de mme nom n on veut dire que o nest pas altr ni dtruit, mais devient e e e e inaccessible car, dans la portion de programme o` le masquage a lieu, n dsigne o , non o. u e 47 Le compilateur lit le programme ` compiler squentiellement, du dbut vers la n. A tout instant il en est ` un certain endroit a e e a du texte source, correspondant ` la position de lunit lexicale courante ; quand la compilation progresse, lunit lexicale avance. a e e Tout cela justie un langage imag, que nous allons employer, avec des expressions comme  le compilateur entre dans la partie e excutable  ou  le compilateur entre (ou sort) dune fonction , etc. e
44 Dans 43 La

42

Lorsque le compilateur traite la dclaration dun identicateur i qui se trouve ` lintrieur dune fonction, e a e i est recherch dans le dictionnaire local exclusivement ; normalement, il ne sy trouve pas (sinon,  erreur : e identicateur dj` dclar ). Suite ` cette dclaration, i est ajout au dictionnaire local. ea e e a e e Il ny a strictement aucun intrt ` savoir si i gure ` ce moment-l` dans le dictionnaire global. ee a a a Lorsque le compilateur traite la dclaration dun identicateur 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 sy trouve pas (sinon,  erreur : identicateur dj` dclar ). Suite ` cette dclaration, i est ajout au dictionnaire ea e e a e e global. Lorsque le compilateur compile une instruction excutable, forcment ` lintrieur dune fonction, chaque e e a e identicateur i rencontr est recherch dabord dans le dictionnaire local ; sil ne sy trouve pas, il est e e recherch ensuite dans le dictionnaire global (si les deux recherches chouent,  erreur : identicateur non e e dclar ). En procdant 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 dutilisation est dtruit, puisque e les objets locaux ne sont pas visibles ` lextrieur 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 dune compilation le dictionnaire global ne diminue jamais. A lintrieur dune e fonction il naugmente pas ; le dictionnaire global naugmente que lorsque le dictionnaire local nexiste pas. 4.2.2 Tableau ` acc`s squentiel a e e

Limplmentation la plus simple des dictionnaires consiste en un tableau dans lequel les identicateurs sont e placs dans lordre o` leurs dclarations ont t trouves dans le texte source. Dans ce tableau, les recherches e u e ee e sont squentielles. Voyez la gure 11 : lorsquil 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 ` lintrieur (a) et ` lextrieur (b) des fonctions a e a e Trois variables sont essentielles dans la gestion du dictionnaire : maxDico est le nombre maximum dentres possibles (` ce propos, voir  Augmentation de la taille du e a dictionnaire  un peu plus loin), e sommet est le nombre dentres valides dans le dictionnaire ; on doit avoir sommet maxDico, ee a base est le premier lment du dictionnaire du dessus (cest-`-dire le dictionnaire local quand il y en a deux, le dictionnaire global quand il ny en a quun). Avec tout cela, la manipulation du dictionnaire devient tr`s simple. Les oprations ncessaires sont : e e e 1. Recherche dun identicateur pendant le traitement dune dclaration (que ce soit ` lintrieur dune e a e fonction ou ` lextrieur de toute fonction) : rechercher lidenticateur dans la portion de tableau comprise a e entre les bornes sommet 1 et base, e 2. Recherche dun identicateur pendant le traitement dune expression excutable : rechercher lidenticateur en parcourant dans le sens des indices dcroissants 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 dun objet global par un objet local de mme nom. e

43

3. Ajout dune entre dans le dictionnaire (que ce soit ` lintrieur dune fonction ou ` lextrieur de toute e a e a e fonction) : apr`s avoir vri que sommet < maxDico, placer la nouvelle entre ` la position sommet, e e e e a puis faire sommet sommet + 1, 4. Creation dun dictionnaire local, au moment de lentre dans une fonction : faire base sommet, e 5. Destruction du dictionnaire local, ` la sortie dune fonction : faire sommet base puis base 0. a Augmentation de la taille du dictionnaire. Une question technique assez agaante quil faut rgler c e lors de limplmentation dun dictionnaire par un tableau est le choix de la taille ` donner ` ce tableau, tant e a a e entendu quon ne conna pas ` lavance la grosseur (en nombre de dclarations) des programmes que notre t a e compilateur devra traiter. La biblioth`que C ore un moyen pratique pour rsoudre ce probl`me, la fonction realloc qui permet dauge e e menter la taille dun espace allou dynamiquement tout en prservant le contenu de cet espace. Voici, ` titre e e a dexemple, la dclaration et les fonctions de gestion dun dictionnaire ralis dans un tableau ayant au dpart la e e e e place pour 50 lments ; chaque fois que la place manque, le tableau est agrandi dautant quil faut pour loger ee 25 nouveaux lments : 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 mmoire)"); 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 mmoire)"); 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 entre 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 mmoire)"); 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

Limplmentation des dictionnaires explique ` la section prcdente est facile ` mettre en uvre et susante e e a e e a pour des applications simples, mais pas tr`s ecace (la complexit des recherches est, en moyenne, de lordre de e e n e 2 , soit O(n) ; les insertions se font en temps constant). Dans la pratique on recherche des implmentations plus ecaces, car un compilateur passe beaucoup de temps49 ` rechercher des identicateurs dans les dictionnaires. a Une premi`re amlioration des dictionnaires consiste ` maintenir des tableaux ordonns, permettant des e e a e recherches par dichotomie (la complexit dune recherche devient ainsi O(log2 n), cest beaucoup mieux). La e gure 11 est toujours valable, mais maintenant il faut imaginer que les lments dindices allant de base ` ee a sommet 1 et, lorsquil y a lieu, ceux dindices allant de 0 ` base 1, sont placs en ordre croissant des a e identicateurs. Dans un tel contexte, voici la fonction existe, qui eectue la recherche de lidenticateur reprsent par ident e e dans la partie du tableau, suppos ordonn, comprise entre les indices inf et sup, bornes incluses. Le rsultat de e e e la fonction (1 ou 0, interprts comme vrai ou faux ) est la rponse ` la question  llment cherch se trouve-t-il ee e a ee e dans le tableau ? . En outre, au retour de la fonction, la variable pointe par ptrPosition contient la position e de llment recherch, cest-`-dire : ee e a si lidenticateur est dans le tableau, lindice de lentre correspondante, e si lidenticateur ne se trouve pas dans le tableau, lindice auquel il faudrait insrer, les cas chant, une e e e entre concernant cet identicateur. 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 entre 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 dune compilation est dpens 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 mmoire)"); 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 xer les ides, voici la fonction qui traite la dclaration dun objet local : e e ... int placement; ... if (existe(lexeme, base, sommet - 1, &placement)) erreurFatale("identificateur dj` dclar"); e a e e else { ici se place lobtention des informations type, adresse, complement, etc. ajouterEntree(placement, lexeme, type, adresse, complement); } ... Nous constatons que lutilisation de tableaux tris permet doptimiser la recherche, dont la complexit passe e e de O(n) ` O(log2 n), mais pnalise les insertions, dont la complexit devient O(n), puisqu` chaque insertion il a e e a faut pousser dun cran la moiti (en moyenne) des lments du dictionnaire. Or, pendant la compilation dun e ee programme il y a beaucoup dinsertions et on ne peut pas ngliger a priori le poids des insertions dans le calcul e du cot de la gestion des identicateurs. u Il y a cependant une tche, qui nest pas la gestion du dictionnaire mais lui est proche, o` on peut sans a u rserve employer un tableau ordonn, cest la gestion dune table de mots rservs, comme celle de la section e e e e 2.2.2. En eet, le compilateur, ou plus prcisment lanalyseur 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 donnes arbre binaire. e Les arbres binaires de recherche ont les avantages des tables ordonnes, pour ce qui est de lecacit de la e e recherche, sans leurs inconvnients puisque, tant des structures cha ees, linsertion dun lment noblige pas e e n ee a ` pousser ceux qui dun 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, vriant la proprit suivante : pour tout nud p de larbre e e ee pour tout nud q appartenant au sous-arbre gauche de p on a qinf o pinf o, pour tout nud r appartenant au sous-arbre droit de p on a rinf o pinf o. Voici, par exemple, lABR obtenu avec les  identicateurs  Denis, Fernand, Bernard, Andr, Gaston, Ernest e et Charles, ajouts ` larbre 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 dlments et, surtout, lordre dinsertion 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 ralis dans un programme comme le montre la gure 12. e e
Denis autres infos

Bernard autres infos autres infos

Fernand

...

...

...

Fig. 12 Ralisation eective des maillons dun ABR e Pour raliser les dictionnaires par des ABR il faudra donc se donner les dclarations : e e typedef struct noeud { ENTREE_DICO info; struct noeud *gauche, *droite; } NOEUD; NOEUD *dicoGlobal = NULL, *dicoLocal = NULL; Voici la fonction qui recherche le nud correspondant ` un identicateur donn. Elle rend ladresse du nud a e cherch, ou NULL en cas dchec 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 utilise pour rechercher des identicateurs apparaissant dans les parties excutables des e e fonctions. Elle sera donc appele de la mani`re suivante : e e ... p = rechercher(lexeme, dicoLocal); if (p == NULL) { p = recherche(lexeme, dicoGlobal); if (p == NULL) erreurFatale("identificateur non dclar"); e e } exploitation des informations du nud point par p e ... Pendant la compilation des dclarations, les recherches se font avec la fonction suivante, qui eectue la e recherche et, dans la foule, lajout dun nouveau nud. Dans cette fonction, la rencontre dun nud associ e e a ` lidenticateur quon cherche est considre comme une erreur grave. La fonction rend ladresse du nud 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 dclar"); 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 dappel (cas des dclarations 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 linsertion est pass par adresse, cest pourquoi il y a deux dans la dclaration 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 sagit soit de dicoLocal soit de dicoGlobal ) vaut NULL et doit changer de valeur. Cest beaucoup de travail pour pas grand-chose, on lviterait en dcidant que les dictionnaires ne sont jamais vides (il sut de leur crer e e e doce un  nud bidon  sans signication). Restitution de lespace alloue. Limplmentation dun dictionnaire par un ABR poss`de lecacit de e e e la recherche dichotomique, car ` chaque comparaison on divise par deux la taille de lensemble susceptible de a contenir llment cherch, sans ses inconvnients, puisque le temps ncessaire pour faire une insertion dans un ee e e e ABR est ngligeable. Hlas, cette mthode a deux dfauts : e e e e la recherche nest dichotomique que si larbre est quilibr, ce qui ne peut tre suppos que si les identie e e e cateurs sont tr`s nombreux et uniformment rpartis, e e e la destruction dun dictionnaire est une opration beaucoup plus complexe que dans les mthodes qui e e utilisent un tableau. La destruction dun dictionnaire, en loccurrence le dictionnaire local, doit se faire chaque fois que le compilateur sort dune fonction. Cela peut se programmer de la mani`re suivante : e void liberer(NOEUD *dico) { if (dico != NULL) { liberer(dico->gauche); liberer(dico->droite); free(dico); } } Comme on le voit, pour rendre lespace occup par un ABR il faut le parcourir enti`rement (alors que dans e e le cas dun tableau la modication dun index sut). Il y a un moyen de rendre beaucoup plus simple la libration de lespace occup par un arbre. Cela consiste e e ae ` crire sa propre fonction dallocation, quon utilise ` la place malloc, et qui alloue un espace dont on ma a trise la remise ` zro. 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 dune table de symboles mrite dtre mentionne ici, car elle est tr`s e e e e e utilise dans les compilateurs rels. Cela sappelle adressage dispers, ou hash-code 51 . Le principe en est assez e e e simple : au lieu de rechercher la position de lidenticateur dans la table, on obtient cette position par un calcul sur les caract`res de lidenticateur ; si on sen tient ` des oprations simples, un calcul est certainement plus e a e rapide quune recherche. Soit I lensemble des identicateurs existant dans un programme, N la taille de la table didenticateurs. Lidal serait de possder une fonction h : I [0, N [ qui serait e e rapide, facile ` calculer, a injective, cest-`-dire qui ` deux identicateurs distincts ferait correspondre deux valeurs distinctes. a a On ne dispose gnralement pas dune telle fonction car lensemble I des identicateurs prsents dans le e e e programme nest pas connu a priori. De plus, la taille de la table nest souvent pas susante pour permettre linjectivit (qui requiert N |I|). e On se contente donc dune fonction h prenant, sur lensemble des identicateurs possibles, des valeurs uniformment rparties sur lintervalle [0, N [. Cest-`-dire que h nest pas injective, mais e e a si N |I|, on esp`re que les couples didenticateurs 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 invitables. Dans ce cas on souhaite quelles soient galement rparties : pour e e e |I| chaque j [0, N [ le nombre de i I tels que h(i) = j est ` peu pr`s le mme, cest-`-dire N . Il est facile a e e a de voir pourquoi : h tant la fonction qui  place  les identicateurs dans la table, il sagit dviter que e e ces derniers samoncellent ` certains endroits de la table, tandis qu` dautres endroits cette derni`re est a a e peu remplie, voire prsente des cases vides. e Il est dicile de dire ce quest une bonne fonction de hachage. La littrature spcialise rapporte de nome e e breuses recettes, mais il ny a probablement pas de solution universelle, car une fonction de hachage nest bonne que par rapport ` un ensemble didenticateurs donn. Parmi les conseils quon trouve le plus souvent : a e prenez N premier (une des recettes les plus donnes, mais plus personne ne se donne la peine den rappeler e la justication), utilisez des fonctions qui font intervenir tous les caract`res des identicateurs ; en eet, 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 rsultat de grandes valeurs ; lorsque ces valeurs sont ramenes ` lintervalle [0, N [, par exemple par une opration de modulo, les ventuels dfauts (dissymtries, accumue e e e lations, etc.) de la fonction initiale ont tendance ` dispara a tre. Une fonction assez souvent utilise consiste ` considrer les caract`res dun identicateur comme les coee a e e cients dun polynme P (X) dont on calcule la valeur pour un X donn (ou, ce qui revient au mme, ` voir un o e e a
51 La technique explique ici est celle dite adressage dispers ouvert. Il en existe une autre, ladressage dispers ferm, dans laquelle e e e e toute linformation se trouve dans le tableau adress par la fonction de hachage (il ny a pas de listes cha ees associs aux cases e n e du tableau).

49

identicateur comme lcriture dun 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 mthodes dadressage dispers dirent entre elles cest dans la mani`re de grer les collisions. a u e e e e e Dans le cas de ladressage dispers ouvert, la table quon adresse ` travers la fonction de hachage nest pas une e a table didenticateurs, mais une table de listes cha ees dont les maillons portent des identicateurs (voyez la n gure 13). Si on note T cette table, alors Tj est le pointeur de tte de la liste cha ee dans laquelle sont les e n identicateurs i tels que h(i) = j. Vu de cette mani`re, ladressage dispers ouvert appara comme une mthode de partitionnement de lene e t e semble des identicateurs. 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 mme taille. Lecacit de la mthode provient alors du fait quau 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 linsertion dun identicateur (suppos absent) : e MAILLON *insertion(char *identif) { int h = hash(identif, N); 50

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

Production de code

Nous nous intressons ici ` la derni`re phase de notre compilateur, la production de code. Dans ce but, e a e nous prsentons certains aspects de larchitecture des machines (registres et mmoire, adresses, pile dexcution, e e e compilation spare et dition de liens, etc.) et en mme 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 limpasse sur certains aspects importants (et diciles) de la gnration de code, e e et notamment sur les algorithmes pour loptimisation du code et pour lallocation des registres.

5.1
5.1.1

Les objets et leurs adresses


Classes dobjets

Dans les langages qui nous intressent les programmes manipulent trois classes dobjets : e 1. Les objets statiques existent pendant toute la dure de lexcution dun programme ; on peut considrer e e e que lespace quils 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 explicite par lauteur du programme (dans les langages qui le permettent) ou bien une e valeur initiale implicite, souvent zro. 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 lespace mmoire dans lequel sont logs les objets statiques. Il est gnralement e e e e constitu de deux zones : la zone du code, o` sont les fonctions et les constantes, et lespace global, o` sont e u u les variables globales. Ladresse dun objet statique est un nombre entier qui indique la premi`re (parfois lunique) cellule de e la mmoire occupe par lobjet. Elle est presque toujours exprime comme un dcalage54 par rapport au e e e e dbut de la zone contenant lobjet en question. e 2. Les objets automatiques sont les variables locales des fonctions, ce qui comprend :
52 En ralit, le compilateur nalloue pas la mmoire pendant la compilation ; au lieu de cela, il en produit une certaine reprsene e e e tation dans le chier objet, et cest un outil appel chargeur qui, apr`s traitement du chier objet par lditeur de lien, installe les e e e objets statiques dans la mmoire. Mais, pour ce qui nous occupe ici, cela revient au mme. e e 53 Lorsque le syst`me le permet, les objets en lecture seule sont logs dans des zones de la mmoire dans lesquelles les tentatives e e e dcriture, cest-`-dire de modication de valeurs, sont dtectes et signales ; les petits syst`mes norent pas cette scurit (qui e a e e e e e e limite les dgts lors des utilisations inadquates des pointeurs et des indices des tableaux). e a e 54 Le mot dcalage (les anglophones disent oset) fait rfrence ` une mthode dadressage employe tr`s souvent : une entit est e ee a e e e e repre par un couple (base,dcalage), o` base est une adresse connue et dcalage un nombre entier quil faut ajouter ` base pour e e e u e a obtenir ladresse voulue. Lacc`s t[i] au i-`me lment dun tableau t en est un exemple typique, dans lequel t (ladresse de t[0]) e e ee est la base et i le dcalage. e

51

les variables dclares ` lintrieur des fonctions, e e a e les arguments formels de ces derni`res. e Ces variables occupent un espace qui nexiste pas pendant toute la dure de lexcution du programme, e e mais uniquement lorsquil est utile. Plus prcisment, lactivation dune fonction commence par lallocation e e dun espace, appel espace local de la fonction, de taille susante pour contenir ses arguments et ses e variables locales (plus un petit nombre dinformations techniques additionnelles expliques en dtail ` la e e a section 5.2.4). Un espace local nouveau est allou chaque fois quune fonction est appele, mme si cette fonction tait e e e e dj` active et donc quun espace local pour elle existait dj` (cest le cas dune fonction qui sappelle ea ea elle-mme, directement ou indirectement). Lorsque lactivation dune fonction se termine son espace local e est dtruit. e

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

Fig. 14 Empilement despaces locaux (f a appel g qui a appel f qui a appel h) e e e La proprit suivante est importante : chaque fois quune fonction se termine, la fonction qui se termine ee est la plus rcemment active de celles qui ont t commences et ne sont pas encore termines. Il en e e ee e e dcoule que les espaces locaux des fonctions peuvent tre allous dans une mmoire gre comme une pile e e e e ee (voyez la gure 14) : lorsquune fonction est active, son espace local est cr au-dessus des espaces locaux e ee des fonctions actives (i.e. commences et non termines) ` ce moment-l`. Lorsquune fonction se termine, e e a a son espace local est celui qui se trouve au sommet de la pile et il sut de le dpiler pour avoir au sommet e lespace local de la fonction qui va reprendre le contrle. o 3. Les objets dynamiques sont allous lorsque le programme le demande explicitement (par exemple ` travers e a la fonction malloc de C ou loprateur new de Java et C++). Si leur destruction nest pas explicitement e demande (fonction free de C, oprateur delete de C++) ces objets existent jusqu` la terminaison du e e a programme55 . Les objets dynamiques ne peuvent pas tre logs dans les zones o` sont les objets statiques, puisque leur e e u existence nest pas certaine, elle dpend des conditions dexcution. Ils ne peuvent pas davantage tre e e e hbergs 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 cration 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 prcdents, appel le tas (heap, en anglais). e e e La gestion du tas, cest-`-dire son allocation sous forme de morceaux de tailles variables, ` la demande du a a programme, la rcupration des morceaux rendus par ce dernier, la lutte contre lmiettement de lespace e e e disponible, etc. font lobjet dalgorithmes savants implments dans des fonctions comme malloc et free e e ou des oprateurs comme new et delete. e En dnitive, la mmoire utilise par un programme en cours dexcution est divise en quatre espaces (voyez e e e e e la gure 15) :
55 Dans certains langages, comme Java, cest un mcanisme de rcupration de la mmoire inutilise qui se charge de dtruire les e e e e e e objets dynamiques dont on peut prouver quils ninterviendront plus dans le programme en cours.

52

tas (objets dynamiques)

espace disponible

pile (objets locaux) espace global code

espace statique

TEG TC

Fig. 15 Organisation de la mmoire e

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

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

Puisque ladresse dun objet est un dcalage par rapport ` la base dun certain espace qui dpend de la e a e classe de lobjet, du point de vue du compilateur  obtenir ladresse dun objet  cest simplement mmoriser e ltat courant dun certain compteur dont la valeur exprime le niveau de remplissage de lespace correspondant. e Cela suppose que dautres oprations du compilateur font par ailleurs grandir ce compteur, de sorte que si un e peu plus tard on cherche ` obtenir ladresse dun autre objet on obtiendra une valeur dirente. a e Plus prcisment, pendant quil produit la traduction dun 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 dunits (des octets, des mots, cela dpend de la machine) du programme en langage machine couramment produites. e Au dbut de la compilation, TC vaut 0. Ensuite, si memoire reprsente lespace (ou le chier) dans lequel e e le code machine est mmoris, la production dun lment de code, comme un opcode ou un oprande, se e e ee e traduit par les deux aectations : memoire[T C] element TC TC + 1
56 En eet, le rle des fonctions de restitution de mmoire (free, delete) est de bien grer les parcelles de mmoire emprunte puis o e e e e rendue par un programme ; ces fonctions limitent la croissance du tas, mais elles ne sont pas censes en rduire la taille. e e

53

Par consquent, pour mmoriser ladresse dune fonction fon il sut de faire, au moment o` commence e e u la compilation de la fonction : adresse(f on) T C TEG (Taille de lEspace Global) Cette variable a constamment pour valeur la somme des tailles des variables globales dont le compilateur a rencontr la dclaration. e e Au dbut de la compilation TEG vaut 0. Par la suite, le rle de TEG dans la dclaration dune variable e o e globale varg se rsume ` ceci : e a adresse(varg) T EG T EG T EG + taille(varg) o` taille(varg) reprsente la taille de la variable en question, qui dpend de son type. u e e e TEL (Taille de lEspace Local) A lintrieur dune fonction, cette variable a pour valeur la somme des tailles des variables locales de la fonction en cours de compilation. Si le compilateur nest pas dans une fonction TEL nest pas dni. e A lentre de chaque fonction TEL est remis ` zro. Par la suite, le rle de TEL dans la dclaration dune e a e o e variable locale varl se rsume ` ceci : e a adresse(varl) T EL T EL T EL + taille(varl) Les arguments formels de la fonction, bien qutant des objets locaux, ninterviennent pas dans le calcul e de TEL (la question des adresses des arguments formels sera traite ` la section 5.2.4). e a A la n de la compilation les valeurs des variables TC et TEG sont prcieuses : e TC est la taille du code donc, dans une organisation comme celle de la gure 15, elle est aussi le dcalage e (relatif ` lorigine gnrale de la mmoire du programme) correspondant ` la base de lespace global, a e e e a TEG est la taille de lespace global ; par consquent, dans une organisation comme celle de la gure 15, e T C + T EG est le dcalage (relatif ` lorigine gnrale de la mmoire du programme) correspondant au e a e e e niveau initial de la pile. 5.1.3 Compilation spare et dition de liens e e e

Tout identicateur apparaissant dans une partie excutable dun programme doit avoir t pralablement e ee e dclar. La dclaration dun identicateur i, que ce soit le nom dune variable locale, dune variable globale ou e e e dune fonction, produit son introduction dans le dictionnaire adquat, 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 dun compilateur les identicateurs 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 chier. Mais les choses sont plus compliques dans les langages, comme C ou Java, o` le texte dun programme peut e u se trouver clat dans plusieurs chiers sources destins ` tre compils indpendamment les uns des autres e e e a e e e (on appelle cela la compilation spare). En eet, dans ces langages il doit tre possible quune variable ou une e e e fonction dclare dans un chier soit mentionne dans un autre. Cela implique qu` la n de la compilation il e e e a y a dans le chier produit quelque trace des noms des variables et fonctions mentionnes dans le chier source. e Notez que cette question ne concerne que les objets globaux. Les objets locaux, qui ne sont dj` pas visibles ea en dehors de la fonction dans laquelle ils sont dclars, ne risquent pas dtre visibles dans un autre chier. e e e Notez galement que le principal intress par cette aaire nest pas le compilateur, mais un outil qui lui e e e est associ, lditeur de liens (ou linker ) dont le rle est de concatner plusieurs chiers objets, rsultats de e e o e e compilations spares, pour en faire un unique programme excutable, en vriant que les objets rfrencs mais e e e e ee e non dnis dans certains chiers sont bien dnis dans dautres chiers, et en compltant de telles  rfrences 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 spare. e e e Nous naurons donc pas besoin dditeur de liens dans notre syst`me. e e
57 Une consquence tangible de la disparition des identicateurs est limpossibilit de  dcompiler  les chiers objets, ce qui e e e nest pas grave, mais aussi la dicult de les dboguer, ce qui est plus embtant. Cest pourquoi la plupart des compilateurs ont e e e une option de compilation qui provoque la conservation des identicateurs avec le code, an de permettre aux dbogueurs daccder e e aux variables et fonctions par leurs noms originaux.

54

Pour les curieux, voici nanmoins quelques explications sur lditeur de liens et la structure des chiers objets e e dans les langages qui autorisent la compilation spare. e e Appelons module un chier produit par le compilateur. Le rle de lditeur de liens est de concatner (mettre o e e bout ` bout) plusieurs modules. En gnral, il concat`ne dun ct les zones de code (objets statiques en lecture a e e e oe seule) de chaque module, et dun autre cot les espaces globaux (objets statiques en lecture criture) de chaque e e module, an dobtenir une unique zone de code totale et un unique espace global total. Un probl`me appara immdiatement : si on ne fait rien, les adresses des objets statiques, qui sont exprimes e t e e comme des dplacements relatifs ` une base propre ` chaque module, vont devenir fausses, sauf pour le module e a a qui se retrouve en tte. Cest facile ` voir : chaque module apporte une fonction dadresse 0 et probablement e a une variable globale dadresse 0. Dans lexcutable nal, une seule fonction et une seule variable globale peuvent e avoir ladresse 0. Il faut donc que lditeur de liens passe en revue tout le contenu des modules quil concat`ne et en corrige e e toutes les rfrences ` des objets statiques, pour tenir compte du fait que le dbut de chaque module ne ee a e correspond plus ` ladresse 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 allg par le fait que les langages-machines supportent deux mani`res de faire rfrence aux objets statiques. e e e ee On dit quune instruction fait une rfrence absolue ` un objet statique (variable ou fonction) si ladresse ee a de ce dernier est indique par un dcalage relatif ` la base de lespace concern (lespace global ou lespace du e e a e code). Nous venons de voir que ces rfrences doivent tre corriges lorsque le module qui les contient est dcal ee e e e e et ne commence pas lui-mme, dans le chier qui sort de lditeur de liens, ` ladresse 0. e e a On dit quune instruction fait une rfrence relative ` un objet statique lorsque ce dernier est repr par un ee a ee dcalage relatif ` ladresse ` laquelle se trouve la rfrence elle-mme. e a a ee e
base

fonction A

fonction A

y
APPELA x APPELR y

Fig. 16 Rfrence absolue et rfrence relative ` une mme fonction A ee ee a e Par exemple, la gure 16 montre le cas dun langage machine dans lequel il y a deux instructions pour appeler une fonction : APPELA, qui prend comme oprande une rfrence absolue, et APPELR qui prend une rfrence e ee ee relative. Lintrt des rfrences relatives saute aux yeux : elles sont insensibles aux dcalages du module dans lequel ee ee e elles se trouvent. Lorsque lditeur de liens concat`ne des modules pour former un excutable, les rfrences e e e ee relatives contenues dans ces modules nont pas ` tre mises ` jour. ae a Bien entendu, cela ne concerne que lacc`s aux objets qui se trouvent dans le mme module que la rfrence ; e e ee il ny a aucun intrt ` reprsenter par une rfrence relative un acc`s ` un objet dun 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 rfrences intra-modules e ee relatives et des rfrences inter-modules absolues. ee Structure des modules. Il nous reste ` comprendre comment lditeur de liens arrive ` attribuer ` une a e a a rfrence ` un objet non dni dans un module (cela sappelle une rfrence insatisfaite) ladresse de lobjet, ee a e ee qui se trouve dans un autre module.

55

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

x y
fonction f

... APPELA ...

???

objets publics ... fonction f ...

Fig. 17 Un module objet Par exemple, la gure 17 reprsente un module contenant la dnition dune fonction publique f et lappel e e dune fonction non dnie dans ce module g. e En rsum, le travail de lditeur de liens se compose schmatiquement des tches suivantes : e e e e a concatnation des sections de code des modules donns ` lier ensemble, e e a dcalage des rfrences absolues contenues dans ces modules (dont les mmorises dans les tables), e ee e e runion des tables dobjets publics et utilisation de la table obtenue pour corriger les rfrences insatisfaites e ee prsentes dans les sections de code, e mission du chier excutable nal, form du code corrig. e e e e

5.2

La machine Mach 1

Nous continuons notre expos sur la gnration de code par la prsentation de la machine Mach 159 , la e e e e machine cible du compilateur que nous raliserons ` titre de projet. e a 5.2.1 Machines ` registres et machines ` pile a a

Les langages volus permettent lcriture dexpressions en utilisant la notation algbrique ` laquelle nous e e e e a sommes habitus, comme X = Y + Z, cette formule signiant  ajoutez le contenu de Y ` celui de Z et rangez e a le rsultat dans X  (X, Y et Z correspondent ` des emplacements dans la mmoire de lordinateur). e a e Une telle expression est trop complique pour le processeur, il faut la dcomposer en des instructions plus e e simples. La nature de ces instructions plus simples dpend du type de machine dont on dispose. Relativement e a ` la mani`re dont les oprations sont exprimes, il y a deux grandes familles de machines : e e e
58 Quels sont les objets publics, cest-`-dire les objets dnis dans un module quon peut rfrencer depuis dautres modules ? a e ee On la dj` dit, un objet public est ncessairement global. Inversement, dans certains langages, tout objet global est public. Dans ea e dautres langages, aucun objet nest public par dfaut et il faut une dclaration explicite pour rendre publics les objets globaux que e e le programmeur souhaite. Enn, dans des langages comme le C, tous les objets globaux sont publics par dfaut, et une qualication e spciale (dans le cas de C cest la qualication static) permet den rendre certains privs, cest-`-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, nots ici R1, R2, etc., qui sont les a e e seuls composants susceptibles dintervenir dans une oprations (autre quun transfert de mmoire ` registre ou e e a rciproquement) ` titre doprandes ou de rsultats. Inversement, nimporte quel registre peut intervenir dans e a e e une opration arithmtique ou autre ; par consquent, les instructions qui expriment ces oprations doivent e e e e spcier leurs oprandes. Si on vise une telle machine, laectation 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 // // // // dplace la valeur de Y dans R1 e dplace la valeur de Z dans R2 e ajoute R1 ` R2 a dplace la valeur de R2 dans X e

2. Les machines ` pile, au contraire, disposent dune pile (qui peut tre la pile des variables locales) au a e sommet de laquelle se font toutes les oprations. Plus prcisment : e e e les oprandes dun oprateur binaire sont toujours les deux valeurs au sommet de la pile ; lexcution e e e dune opration binaire consiste toujours ` dpiler deux valeurs x et y et ` empiler le rsultat x y de e a e a e lopration, e loprande dun oprateur unaire est la valeur au sommet de la pile ; lexcution dune opration unaire e e e e consiste toujours ` dpiler une valeur x et ` empiler le rsultat x de lopration. 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 plutt que pour une e a o machine ` registres, mais ce dfaut est largement corrig dans les compilateurs commerciaux par lutilisation a e e de savants algorithmes qui optimisent lutilisation des registres. Car il faut savoir que les machines  physiques  existantes sont presque toujours des machines ` registres, a pour une raison decacit facile ` comprendre : les oprations qui ne concernent que des registres restent e a e internes au microprocesseur et ne sollicitent ni le bus ni la mmoire de la machine60 . Et, avec un compilae teur optimisateur, les expressions complexes sont traduites par des suites dinstructions dont la plupart ne mentionnent que des registres. Les algorithmes doptimisation et dallocation des registres sortant du cadre de ce cours, la machine pour laquelle nous gnrerons du code sera une machine ` pile. e e a 5.2.2 Structure gnrale de la machine Mach 1 e e

La mmoire de la machine Mach 1 est faite de cellules numrotes, organises comme le montre la gure e e e e 18 (cest la structure de la gure 15, le tas en moins). Les registres suivants ont un rle essentiel dans le o fonctionnement de la machine : CO (Compteur Ordinal) indique constamment la cellule contenant linstruction que la machine est en train dexcuter, e BEG (Base de lEspace Global) indique la premi`re cellule de lespace rserv aux variables globales (autree e e ment dit, BEG pointe la variable globale dadresse 0), e BEL (Base de lEspace Local) indique la cellule autour de laquelle est organis lespace local de la fonction en cours dexcution ; la valeur de BEL change lorsque lactivation dune fonction commence ou nit (` 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, cest-`-dire le nombre total de cellules occupes dans la mmoire. a e e
60 Se rappeler que de nos jours, dcembre 2001, le microprocesseur dun ordinateur personnel moyennement puissant tourne ` 2 e a GHz (cest sa cadence interne) alors que son bus et sa mmoire ne travaillent qu` 133 MHz. e a

57

espace disponible SP pile

BEL

espace global BEG CO code

Fig. 18 Organisation de la mmoire de la machine Mach 1 e

5.2.3

Jeu dinstructions

La machine Mach 1 est une machine ` pile. a Chaque instruction est faite soit dun seul opcode, elle occupe alors une cellule, soit dun opcode et un oprande, elle occupe alors deux cellules conscutives. Les constantes enti`res et les  adresses  ont la mme e e e e taille, qui est celle dune cellule. La table 1, ` la n de ce polycopi, donne la liste des instructions. a e 5.2.4 Complments sur lappel des fonctions e

La cration et la destruction de lespace local des fonctions a lieu ` quatre moments bien dtermins : e a e e lactivation de la fonction, dabord du point de vue de la fonction appelante (1), puis de celui de la fonction appele (2), ensuite le retour, dabord du point de vue de la fonction appele (3) puis de celui de la fonction e e appelante (4). Voici ce qui se passe (voyez la gure 19) : 1. La fonction appelante rserve un mot vide sur la pile, o` sera dpos le rsultat de la fonction, puis e u e e e elle empile les valeurs des arguments eectifs. Ensuite, elle excute une instruction APPEL (qui empile e ladresse de retour). 2. La fonction appele empile le  BEL courant  (qui est en train de devenir  BEL prcdent ), prend la e e e valeur de SP pour BEL courant, puis alloue lespace local. Pendant la dure de lactivation de la fonction : e les variables locales sont atteintes ` travers des dplacements (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 dplacements (ngatifs, 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 dposer son rsultat est atteinte elle aussi ` travers un dplacement relatif a ` BEL. Ce dplacement est (n + 3), n tant le nombre darguments formels, et suppose donc laccord e e entre la fonction appelante et la fonction appele au sujet du nombre darguments de la fonction appele. e e Lensemble de toutes ces valeurs forme l espace local  de la fonction en cours. Au-del` de cet espace, la a pile est utilise pour le calcul des expressions courantes. e 3. Terminaison du travail : la fonction appele remet BEL et SP comme ils taient lorsquelle a t active, e e ee e puis eectue une instruction RETOUR. e e 4. Reprise du calcul interrompu par lappel. La fonction appelante lib`re lespace occup par les valeurs des arguments eectifs. Elle se retrouve alors avec une pile au sommet de laquelle est le rsultat de la fonction e qui vient dtre appele. Globalement, la situation est la mme quapr`s une opration arithmtique : les e e e e e e oprandes ont t dpils et remplacs par le rsultat. e ee e e e e

58

haut de la pile

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

BEL

Fig. 19 Espace local dune fonction

Qui cree lespace local dune fonction ? Les arguments dune fonction et les variables dclares ` e e a lintrieur de cette derni`re sont des objets locaux et ont donc des adresses qui sont des dplacements relatifs ` e e e a BEL (positifs dans le cas des variables locales, ngatifs dans le cas des arguments, voir la gure 19). Lexplication e prcdente montre quil y a une dirence importante entre ces deux sortes dobjets locaux : e e e les arguments sont installs dans la pile par la fonction appelante, car ce sont les valeurs dexpressions e qui doivent tre values 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 appele, e e e les variables locales sont alloues par la fonction appele, qui est la seule ` en conna le nombre ; cest e e a tre donc cette fonction qui doit les enlever de la pile, lorsquelle se termine (linstruction SORTIE fait ce travail). Les conventions dappel. Il y a donc un ensemble de conventions, dites conventions dappel, qui prcisent e a ` quel endroit la fonction appelante doit dposer les valeurs des arguments et o` trouvera-t-elle le rsultat ou, e u e ce qui revient au mme, ` quel endroit la fonction appele trouvera ses arguments et o` doit-elle dposer son e a e u e rsultat. e En gnral les conventions dappel ne sont pas laisses ` lapprciation des auteurs de compilateurs. Edictes e e e a e e par les concepteurs du syst`me dexploitation, elles sont suivies par tous les compilateurs  homologus , ce e e qui a une consquence tr`s importante : puisque tous les compilateurs produisent des codes dans lesquels les e e param`tres et le rsultat des fonctions sont passs de la mme mani`re, une fonction crite dans un langage e e e e e e L1 , et compile donc par le compilateur de L1 , pourra appeler une fonction crite dans un autre langage L2 et e e compile par un autre compilateur, celui de L2 . e Nous avons choisi ici des conventions dappel simples et pratiques (beaucoup de syst`mes font ainsi) puisque : e e e les arguments sont empils dans lordre dans lequel ils se prsentent dans lexpression dappel, le rsultat est dpos 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 inconvnient61 . Dans notre syst`me, les argument arg1 , arg2 , ... argn sont atteints, ` e e a lintrieur de la fonction, par les adresses locales respectives (n + 3) + 1, (n + 3) + 2, ... (n + 3) + n = 3, et e le rsultat de la fonction lui-mme poss`de ladresse locale (n + 3). Cela oblige la fonction appele ` conna e e e e a tre
61 Et encore, ce nest pas sur que ce soit un inconvnient, tellement il semble naturel dexiger, comme en Pascal ou Java, que la e fonction appelante et la fonction appele soient daccord sur le nombre darguments de cette derni`re. e e

59

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

5.3

Exemples de production de code

Quand on nest pas connaisseur de la question on peut croire que la gnration de code machine est la partie e e la plus importante dun compilateur, et donc que cest elle qui dtermine la structure gnrale de ce dernier. e e e On est alors surpris, voire frustr, en constatant la place considrable que les ouvrages spcialiss consacrent ` e e e e a lanalyse (lexicale, syntaxique, smantique, etc.). Cest que62 : e La bonne mani`re dcrire 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 oprations qui produisent e e du code pour la machine M . Dans cette section nous expliquons, ` travers des exemples, comment ajouter ` notre analyseur les oprations a a e de gnration de code qui en font un compilateur. Il faudra se souvenir de ceci : quand on rchit ` la gnration e e e e a e e de code on est concern par deux excutions direntes : dune part lexcution du compilateur que nous ralisons, e e e e e qui produit comme rsultat un programme P en langage machine, dautre part lexcution du programme P ; a e e priori cette excution a lieu ultrieurement, mais nous devons limaginer en mme temps que nous crivons le e e e e compilateur, pour comprendre pourquoi nous faisons que ce dernier produise ce quil produit. 5.3.1 Expressions arithmtiques e

Puisque la machine Mach 1 est une machine ` pile, la traduction des expressions arithmtiques, aussi a e complexes soient-elles, est extrmement simple et lgante. A titre dexemple, imaginons que notre langage e ee source est une sorte de  C francis , et considrons la situation suivante (on suppose quil ny a pas dautres e e dclarations de variables que celles quon voit ici) : e entier x, y, z; ... entier uneFonction() { entier a, b, c; ... y = 123 * x + c; } Pour commencer, intressons-nous ` lexpression 123 * x + c. Son analyse syntaxique aura t faite par e a ee des fonctions expArith (expression arithmtique) et nExpArith ressemblant ` ceci : e a void expArith(void) { terme(); finExpArith(); } void finExpArith(void) { if (uniteCourante == + || uniteCourante == -) { uniteCourante = uniteSuivante(); terme(); finExpArith(); } } (les fonctions terme et nTerme sont analogues aux deux prcdentes, avec et / dans les rles de + et e e o , et facteur dans le rle de terme). Enn, une version simplie63 de la fonction facteur pourrait commencer o e comme ceci :
gnralement : la bonne faon dobtenir linformation porte par un texte soumis ` une syntaxe consiste ` crire lanalyseur e e c e a ae syntaxique correspondant et, dans un deuxi`me temps, ` lui ajouter les oprations qui construisent les informations en question. e a e Ce principe est tout ` fait fondamental. Si vous ne deviez retenir quune seule chose de ce cours, que ce soit cela. a 63 Attention, nous envisageons momentanment un langage ultra-simple, sans tableaux ni appels de fonctions, dans lequel une e occurrence dun identicateur 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 dune machine ` pile, expliqu ` la section 5.2.1, a la consquence fondamentale a ea e suivante : 1. La compilation dune expression produit une suite dinstructions du langage machine (plus ou moins longue, selon la complexit de lexpression) dont lexcution a pour eet global dajouter une valeur au e e sommet de la pile, ` savoir le rsultat de lvaluation de lexpression. a e e 2. La compilation dune instruction du langage source produit une suite dinstructions du langage machine (plus ou moins longue, selon la complexit de lexpression) dont lexcution laisse la pile dans ltat o` e e e u elle se trouvait avant de commencer linstruction en question. Une mani`re de retrouver quel doit tre le code produit pour la compilation de lexpression 123 * x + c e e consiste ` se dire que 123 est dj` une expression correcte ; leet dune telle expression doit tre de mettre au a ea e sommet de la pile la valeur 123. Par consquent, la compilation de lexpression 123 doit donner le code e EMPC 123 De mme, la compilation de lexpression x doit donner e EMPG EMPL 0 2 (car x est, dans notre exemple, la premi`re variable globale) et celle de lexpression c doit donner e (car c est la troisi`me variable locale). Pour obtenir ces codes il sut 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 lment (un opcode ou un oprande) dans le code. Si nous ee e supposons que ce dernier est rang dans la mmoire, 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 nont pas besoin dtre modies. Seules les e e fonctions dans lesquelles des oprateurs apparaissent explicitement doivent ltre : 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 nal, le code produit ` loccasion de la compilation de lexpression 123 * x + c sera donc : a EMPC 123 EMPG 0 MUL EMPL 2 ADD et, en imaginant lexcution de la squence ci-dessus, on constate que son eet global aura bien t dajouter e e ee une valeur au sommet de la pile. Considrons maintenant linstruction compl`te y = 123 * x + c. Dans un compilateur ultra-simpli qui e e e ignorerait les appels de fonction et les tableaux, on peut imaginer quelle aura t analyse par une fonction ee e instruction commenant comme ceci : c void instruction(void) { if (uniteCourante == IDENTIFICATEUR) { uniteCourante = uniteSuivante(); terminal(=); expArith(); terminal(;); } else ... } /* instruction daffectation */

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

on voit alors que le code nalement produit par la compilation de y = 123 * x + c aura t : ee EMPC 123 EMPG 0 MUL EMPL 2 ADD DEPG 1 (y est la deuxi`me variable globale). Comme prvu, lexcution du code prcdent laisse la pile comme elle tait e e e e e e en commenant. c 5.3.2 Instruction conditionnelle et boucles

La plupart des langages modernes poss`dent des instructions conditionnelles et des  boucles tant que  e dnies 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 lanalyseur syntaxique correspondant ` la r`gle prcdente 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 ... } Commenons par le code ` gnrer ` loccasion dune boucle  tant que . Dans ce code on trouvera les suites c a e e a dinstructions gnres pour lexpression et pour linstruction que la syntaxe prvoit (ces deux suites peuvent e ee e tre tr`s longues, cela dpend de la complexit de lexpression et de linstruction en question). e e e e Leet de cette instruction est connu : lexpression est value ; si elle est fausse (c.-`-d. nulle) lexcution e e a e continue apr`s le corps de la boucle ; si lexpression est vraie (c.-`-d. non nulle) le corps de la boucle est excut, e a e e puis on revient ` lvaluation de lexpression et on recommence tout. a e Notons expr1 , expr2 , ... exprn les codes-machine produits par la compilation de lexpression et instr1 , instr2 , ... instrk ceux produits par la compilation de linstruction. Pour linstruction  tant que  toute enti`re on aura e donc un code comme ceci (les rep`res dans la colonne de gauche, comme et , reprsentent les adresses des e e instructions) :
64 Notez que, dans le cas de linstruction conditionnelle, lutilisation dun analyseur rcursif descendant nous a permis de faire e une factorisation ` gauche originale du dbut commun des deux formes de linstruction 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 dicile, ici, est de raliser quau moment o` le compilateur doit produire linstruction e u SIFAUX , ladresse nest pas connue. Il faut donc produire quelque chose comme SIFAUX 0 et noter que la case dans laquelle est inscrit le 0 est ` corriger ultrieurement (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  rparer 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 mmoire ; elle serait considrablement e e plus complexe si le code tait produit dans un chier) : e void repCode(int place, int valeur) { mem[place] = valeur; } Instruction conditionnelle. Dans le mme ordre dides, voici le code ` produire dans le cas de linse e a truction conditionnelle ` une branche ; il nest pas dicile dimaginer ce quil faut ajouter, et o`, dans lanalyseur a u montr plus haut pour lui faire produire le code suivant : e ... expr1 dbut de linstruction si...alors... e expr2 ... exprn SIFAUX instr1 instr2 ... instrk ... la suite (apr`s linstruction si...alors...) e 64

Et voici le code ` produire pour linstruction 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 ... dbut de linstruction si...alors...sinon... e

dbut de la premi`re branche e e

dbut de la deuxi`me branche e e

5.3.3

la suite (apr`s linstruction si...alors...sinon...) e

Appel de fonction

Pour nir cette galerie dexemples, examinons quel doit tre le code produit ` loccasion de lappel dune e a fonction, aussi bien du ct de la fonction appelante que de celui de la fonction appele. oe e Supposons que lon a dune 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 lon a dautre part le code (u, v, w sont les seules variables globales de ce programme) : entier u, v, w; ... entier principale() { ... u := 200 - distance(v, w / 25 - 100) * 2; ... } Voici le segment du code de la fonction principale qui est la traduction de laectation prcdente : 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 rsultat de la fonction e premier argument

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

ceci ach`ve le calcul du membre droit de laectation e

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

allocation de lespace local (une variable)

debut de linstruction si

] ] simulation du unaire ]

le rsultat de la fonction e

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

66

Tab. 1 Les instructions de la machine Mach 1 opcode EMPC EMPL oprande e valeur adresse explication EMPiler Constante. Empile la valeur indique. e EMPiler la valeur dune variable Locale. Empile la valeur de la variable dtermine par le dplacement relatif ` BEL donn par adresse (entier relae e e a e tif). DEPiler dans une variable Locale. Dpile la valeur qui est au sommet et la e range dans la variable dtermine par le dplacement relatif ` BEL donn par e e e a e adresse (entier relatif). EMPiler la valeur dune variable Globale. Empile la valeur de la variable dtermine par le dplacement (relatif ` BEG) donn par adresse. e e e a e DEPiler dans une variable Globale. Dpile la valeur qui est au sommet et la e range dans la variable dtermine par le dplacement (relatif ` BEG) donn e e e a e par adresse. EMPiler la valeur dun lment de Tableau. Dpile 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 dtermine par le dplacement (relatif ` BEG) indiqu a e e e a e par adresse. DEPiler dans un lment de Tableau. Dpile 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 dtermine par le dplacement (relatif ` BEG) indiqu par adresse. e e e a e ADDition. Dpile deux valeurs et empile le rsultat de leur addition. e e SOUStraction. Dpile deux valeurs et empile le rsultat de leur soustraction. e e MULtiplication. Dpile deux valeurs et empile le rsultat de leur multiplication. e e DIVision. Dpile deux valeurs et empile le quotient de leur division euclidienne. e MODulo. Dpile deux valeurs et empile le reste de leur division euclidienne. e Dpile deux valeurs et empile 1 si elles sont gales, 0 sinon. e e INFerieur. Dpile deux valeurs et empile 1 si la premi`re est infrieure ` la e e e a seconde, 0 sinon. INFerieur ou EGal. Dpile deux valeurs et empile 1 si la premi`re est infrieure e e e ou gale ` la seconde, 0 sinon. e a Dpile une valeur et empile 1 si elle est nulle, 0 sinon. e Obtient de lutilisateur un nombre et lempile ECRIre Valeur. Extrait la valeur qui est au sommet de la pile et lache Saut inconditionnel. Lexcution continue par linstruction ayant ladresse ine dique. e Saut conditionnel. Dpile une valeur et si elle est non nulle, lexcution contie e nue par linstruction ayant ladresse indique. Si la valeur dpile est nulle, e e e lexcution continue normalement. e Comme ci-dessus, en permutant nul et non nul. Appel de sous-programme. Empile ladresse de linstruction suivante, puis fait la mme chose que SAUT. e Retour de sous-programme. Dpile une valeur et continue lexcution par linse e truction dont cest ladresse. Entre dans un sous-programme. Empile la valeur courante de BEL, puis copie e la valeur de SP dans BEL. Sortie dun sous-programme. Copie la valeur de BEL dans SP, puis dpile une e valeur et la range dans BEL. Allocation et restitution despace dans la pile. Ajoute nbreMots, qui est un entier positif ou ngatif, ` SP e a La machine sarrte. 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

Vous aimerez peut-être aussi