Anda di halaman 1dari 75

Compilation

Frdrique Carrere
8 septembre 2014

Chapitre 1

Introduction
1.1
1.1.1

Quest-ce quun compilateur ?


Dfinition

Un compilateur est un logiciel qui traduuit un programme crit dans un


langage source vers un programme crit dans un langage cible.
Langage L Compilateur Langage L
Dordinaire L est un langage de programmation de haut niveau, comme C++
ou Java par exemple, et L est un langage de bas niveau, par exemple un
code machine adapt un ordinateur en particulier.
Exemples :
de C vers de lassembleur : gcc
de Java vers du byte code : javac
de latex vers du postcript ou du html : latex2html

1.1.2

Rle du compilateur

Son rle est de permettre de programmer avec un haut niveau dabstraction (ex : langages ddis) plutt quen assembleur. Il permet donc dcrire
des programmes lisibles, modulaires, maintenables, rutilisables.
Il permet de sabstraire de larchitecture de la machine et donc dcrire des
programmes portables.
Il permet de dtecter des erreurs et donc dcrire des programmes plus fiables.
On peut crire des compilateurs certifis par des logiciels de vrifications

comme coq.
Ses principale tches sont :
lire et analyser le programme
dtecter des erreurs (lexicales, syntaxiques, smantiques)
crire dans un langage intermdiaire la squence dinstructions effectuer par la machine
optimiser cette squence dinstructions (taille du code, vitesse dxecution)
traduire ces instructions dun langage intermdiaire dans le langage
cible

1.1.3

Techniques mises en jeu

Le compilateur fait intervenir de nombreux domaines de linformatique :


informatique thorique : automates, grammaires, langages
structures de donnes : tables de symboles, arbres de syntaxe abstraite
algorithmes : algorithmes doptimisation, programmation dynamique,
coloration de graphes,
architecture des ordinateurs : organisation de la mmoire, allocations,
slection dinstructions
gnie logiciel : modularit, utilisation de design patterns, dun environnement de dveloppement (eclipse)
logique : systmes de typage
intelligence artificielle : heuristiques doptimisations
Dautres logiciels sont de proches parents des compilateurs : interprteurs
(python, shell), traducteurs de langages naturels, correcteurs dorthographe.
Les techniques utilises en compilation sont aussi utilises par les prprocesseurs pour lexpansion des macros, les diteurs de textes comme emacs,
les interprtes de requtes dans les bases de donnes. En conclusion, ltude
des compilateurs permet de mieux comprendre un certains nombre doutils
familiers du programmeur, ainsi quun certain nombres de notions cruciales
telles que la porte des variables, le typage, le polymorphisme.

1.2

Les phases de la compilation

Un compilateur se dcoupe en trois grandes parties :


le front-end : dpendant du langage source

le coeur qui travaille sur une reprsentation intermdiaire du programme, indpendante la fois du langage source et du langage cible
le back-end : fortement li au langage cible
Lavantage de ce dcoupage est la rutilisabilit du coeur pour diffrents langages sources et pour diffrents langages cibles.

1.2.1

Les diffrents modules

La production dun excutable comprend une succession dtapes importantes, implmente par diffrents modules :
prprocesseur
analyseur lexical
analyseur syntaxique
gestion dune tables de symboles
analyse smantique, typage
gnrateur de code intermdiaire
optimisation de code
gnrateur de code assembleur
diteur de liens
Nous distinguons le compilateur des outils qui sont utiliss en amont : Editeur, Prprocesseur, et des outils qui sont utiliss en aval : Assembleur, lieur,
chargeur.
Sources -> Prprocesseur -> Programme source -> Compilateur -> programme cible -> Assembleur -> Lieur-chargeur

1.2.2

Les performances

Les performances diffrent dun compilateur un autre :


Detections des erreurs plus ou moins performante,
3

Optimisations de code plus ou moins pousses,


Scurit plus ou moins pousse,
Production dinstructions plus ou moins structures. Lassembleur par
exemple est trs peu structur, il prsente des instructions machines,
des directives, des tiquettes.

1.2.3

Les Entres/sorties

En entre :
des donnes : variables, structures, tableaux, pointeurs, classes
des instructions de haut niveau : boucles, conditionnelles, fonctions
En sortie :
registres, adresses mmoires, tiquettes
instructions de bas niveau : lecture/criture de registres, oprations
arithmtiques ou logiques de base, sauts, directives (appel de fonctions)

1.3

Exemple : PGCD

int PGCD(int a , int b )


{
while ( b != a ) {
if ( a > b )
a = a-b;
else {
/* Echanger a et b */
int tmp;
tmp = a;
a = b;
b = tmp;
}
return a;
}
1. Analyse lexicale :
Elle spcifie les rgles dcriture des mots du langage source.
Identifie les commentaires, mots rservs, oprateurs, constantes, identificateurs.
2. Analyse syntaxique :
Elle spcifie la manire de construire des textes dans le langage source.

ELle identifie les instructions structures, les blocs imbriqus, les dclarations, et transforme le code en arbre.
3. Analyse smantique, gnration de code intermdiaire :
Elle analyse la signification du texte, que reprsentent les identificateurs, quelles oprations sont associes aux instructions.
- Types :
P GCD int int int
a int
b int
- Instructions :
SUCC(WHILE(TEST (!=, a, b), IF (TEST (>, a, b), AFF (a, OP (-, a, b)),
SUCC(BLOC(VAR(tmp, int), AFF (tmp, a), SUCC(AFF (a, b), AFF (b,
tmp)))))),
RETURN (a))

Chapitre 2

Analyse lexicale
Rle : grouper les lettres pour former des mots.
Les mots sont appels lexmes (ou token).
Au passage, cette phase :
peut reconnatre les mots rservs, constantes, identificateurs
signale les mots mal orthographis
peut supprimer les commentaires.
peut prprocesser le texte : expansion de macros.

2.1

Entits lexicales (token)

Dfinition par des expressions rationnelles. Lanalyseur est un automate


fini dont les tats terminaux sont associs des actions (traduction, mission
dun code caractrisant le lxme).

2.1.1

Expression rationnelle

Soit un alphabet A, un ensemble rationnel est :


1. Ensemble fini de mots
2. Concatnation densembles rationnels R1 R2
3. Itration densembles rationnels R
4. Union densembles rationnels R1 R2
Exemple :
Alphabet = [A-Z]|[a-z]|[0-9]|"_";
Lettre = [A-Z]|[a-z]
Chiffre = [0-9]
6

Identficateur = {Lettre} ({Lettre} | {Chiffre} |


Entier = {0} | [1-9]{Chiffre}*

2.1.2

"_") *

Automate

Un analyseur lexical simplmente laide dun automate fini. Lautomate


doit tre dterministe. Il doit reconnatre lensemble des lexmes. Lautomate
a un comportement particulier : il ne sarrte pas au premier tat final rencontr, il le mmorise (avec la position dans le texte dentre atteinte dans
cet tat) et continue la reconnaissance jusqu ltat final premettant de lire
le plus grand nombre de caractres du programme source.

2.2

JFLEX : fonctionnement

Fichier flex -> JFlex -> Lexer.java -> Java -> Lexer.class
Lanalyseur lexical reconnait des ensembles de mots. Chaque ensemble de
mots (lexme) est dcrit par une expression rationnelle ou expression rgulire.
Lanalyseur lexical associe chaque expression une action (traduction, spcification du lexme,...). Pour Jflex cette action est du code Java.
Lanalyseur lexical produit par Jflex lit le fichier dentre, identifie les mots
appartenant aux ensembles dcrits par les expressions rgulires et pour
chaque mot reconnu, effectue une sries dactions crites en Java dans le
fichier source de lanalyseur : lexer.jflex.

2.2.1

Format dun fichier source jflex :

Il contient trois sections spares par le signe %%


section 1 : le nom du paquetage, les importations dautres paquetages.
section 2 : des options (%cup, %public, %line), des dfinitions de variables et fonctions utiles encadres par les symboles %{ et %}.
section 3 : les expressions rationnelles et les actions associes chaque
expression.
%{
// Code de dfinition java
%}
7

<partie dfinitions>
%%
<partie rgles>
expression { <code java> }
%%
<partie code utilisateur (java)>

Dans la classe java Lexer gnre par Jflex, la fonction danalyse lexicale
sappelle yylex().
Exemple dappel de la fonction danalyse lexicale :

public static void main(String[] args) {


try {
FileReader myFile = new FileReader(args[0]);
Lexer myLex = new Lexer(myFile);
return lexer.yylex();
}
catch (Exception e){
System.out.println("invalid file");
}
}
Le texte compris entre %{ et %} est recopi par jflex dans le source java
gnr avant yylex().
Lorsque yylex() est appele, elle cherche dans lentre la rgle dont lexpression reconnat le plus long motif (en considrant aussi ltat courant).
Si plusieurs rgles reconnaissent ce plus long motif, la premire apparaissant
dans le source jflex est applique. Si aucune rgle ne sapplique, les caractres
lus en entre et qui ne sont pas reconnus sont recopis sur la sortie.
Lorsque une chane de caractres est reconnue par lanalyseur elle est
retourne par la fonction yytext(), et sa longueur est retourne par la fonction
yylength().

2.2.2

Expressions rationnelles dans Jflex :

a : le caractre a
si e,e0 sont deux expressions :
e|e0 : soit e, soit e
ee0 : e suivie de e
si e est une expression :
e : un nombre arbitraire de e.
e+ : au moins une occurrence de e.
e? : zro ou une occurrence de e.
e : jusqu une occurrence de e.
e{n} : n occurrences de e.
e{n, m} : de n m occurrences de e.

2.2.3

Dfinition dans Jflex :

Les ensembles rationnels peuvent tre spcifis sous la forme de macros :


<nom> <expression>
Dans la partie rgles, on pourra utiliser {nom} pour dsign lensemble rationnel dfini par < expression >.
Exemple
import java.io.*;
%%
%public
%class Lexer
%standalone
%8bit

%{
StringBuffer str = new StringBuffer();
%}
LineTerminator = \r|\ n|\ r\ n
InputCharacter = [\^\ n\ r]
WhiteSpace = \{LineTerminator\}|[ \ f\ t]
\%\%
/* Keywords */
if { System.out.printf("KEYWORD:\%s\ n", yytext());}
else {System.out.printf("KEYWORD:\%s\ n", yytext());}
/* Operators */
"+" {System.out.printf("OPERATOR:\%s\ n", yytext());}
/* Literals */
/* Comments and whitespace */
{WhiteSpace} {/* Nothing */}

2.2.4

Etats :

Rle : appliquer des rgles de faon conditionnelle.


Si lanalyseur lexical possde des tats E1, E2,..., tout moment, il se trouve
dans un tat Ei.
Au dpart, il est dans ltat nomm Y Y IN IT IAL. Lappel de la fonction
yybegin(Ei) le met dans ltat Ei. On peut crire des rgles qui ne sappliquent que dans un tat bien prcis.
Dfinition des tats (dans la partie 1)
%state < nom_etat > : tat inclusif.
%xstate < nom_etat > : tat exclusif.
Source lex :
%{
// Code java
%}
%state etat1
%xstate etat2
...

10

%%
...
Lorsque lanalyseur est dans ltat etat1, les seules rgles actives sont :
. celles prfixes par <etat1>,
. celles non prfixes.
Lorsque lanalyseur est dans ltat etat2, les seules rgles actives sont celles
prfixes par <etat2>.
Exemple :
%x SPECIAL
specialmotif expression1
endmotif
expression2
%%
{specialmotif}
{yybegin(SPECIAL);}
<SPECIAL>blabla do_something();
{endmotif}
{yybegin(YYINITIAL);}

11

Chapitre 3

Analyse syntaxique
3.1

Introduction

Les langages rguliers sont insuffisants pour spcifier la structure dun


programme. Il est ncessaire de reconnatre des constructions imbriques,
comme les blocs, les boucles, les expressions arithmtiques parenthses...
Pour ces constructions il est ncessaire davoir une pile.
Equivalence :

langages rationnels automates finis


langages algbriques automates pile

Exemple : {an bn , n 0} est algbrique, mais nest pas rgulier (a reprsent


par exemple laccolade ouvrante et b laccolade fermante).
On peut donner un automate pile pour reconnatre ce langage : la lecture de laccolade ouvrante, il la met sur la pile ; la lecture de laccolade
fermante, il dpile laccolade ouvrante correspondante.
On donne au constructeur danalyseur syntaxique une grammaire algbrique,
cest dire une liste de rgles de grammaire.
Exemple :
axiom -> prog <EOF>
| <EOF>
prog -> decl_list fonc_list
| fonc_list
decl_list -> decl_list decl
| decl
12

fonc_list

fonc

-> fonc_list fonc


| fonc

-> return_type fonc_name ( args_list ) bloc

return_type -> int | double | ...


On peut reprsenter les rgles utilises pour construire un programme donn
par un arbre dont la racine est axiom. Cet arbre est appel arbre de drivation ou arbre syntaxique.
Le constructeur danalyseur syntaxique traduit la grammaire en un automate
pile.
Ide : :
- commencer par mettre le symbole de dbut start sur la pile.
- chaque fois quun symbole X est en sommet de pile et quil existe une rgle
X -> u dans la grammaire, remplacer X par u sur la pile.
- dpiler chaque fois que le symbole sur la pile est le mme que le symbole
lu dans le programme source.
Mais lapplication nave de ce principe donne un automate pile fortement
non-dterministe. Il faut donc utiliser des mthodes un peu plus labores.

3.2
3.2.1

Trois grandes classes danalyseurs syntaxique


Analyseurs descendants

Lanalyseur essaie de reconstituer larbre syntaxique partir de la racine


start et de la branche gauche.
mthode LL (Leftmost generation of a Leftmost derivation)
Cette mthode ne sapplique quavec une classe restreinte de grammaires
(ce sont des grammaires non-ambiges et non rcursives gauches). Donc ne
convient pas pour la plupart des langages de programmation.
Lalgorithme de reconnaissance dun programme est alors en temps linaire.

3.2.2

Analyseurs ascendants

Lanalyseur essaie de reconstituer larbre syntaxique partir des feuilles


et de la branche gauche.
mthode LR (Leftmost generation of a Rightmost derivation). Ces analyseurs ne sappliquent galement qu des grammaires non-ambiges (pas
13

C++). Mais les logiciels (cup ou yacc) peuvent traiter des grammaires ambiges si on leur ajoute des rgles de priorits.
Lalgorithme de reconnaissance dun programme est alors en temps n3 .

3.2.3

Analyseurs tabulaires

Coke-Younger-Kasami (bottom-up) : en n3 , peu utilis en pratique


(introduit un certain nombre ditems inutiles).
Earley (top-down) : en n3 , en n2 si la grammaire est non-ambige,
linaire avec la plupart des grammaires LR. Autre avantage : dtecte
immdiatement la lecture dun symbole, si le mot lu jusqualors est
prefixe dau moins un mot engendr par la grammaire.
GLR (LR Generalise) utilise pour les langages naturels (et C++) :
en n3 , linaire si la grammaire est non-ambige, (sinon duplication
de lautomate si on arrive dans un tat non-dterminisme, duplication
de la pile aussi mais avec partage de parties communes -> utilise des
dags).

3.3

Rle de lanalyseur

- analyser la structure du programme source.


- relev les erreurs syntaxiques.
- La rcupration sur erreurs : il faut tre capable de continuer lanalyse mme
si le code source contient des erreurs (ou pour les langues naturelles, si la
grammaire est incomplte). Il sagira de dtecter les problmes et produire
une analyse mme imparfaite. Dans le cas des grammaires ambiges, on
peut choisir une tansition de lautomate et continuer lanalyse : si lautomate
tombe dans ltat erreur, il faudra revenir en arrire et essayer avec une autre
action.
Quatre approches possibles :
mode panique : supprime 1 1 les symboles du progamme source qui
font problme jusqu se rattraper.
production derreurs (cup, yacc) : rajoute une rgle de grammaire
contenant la notion derreur, par exemple : expression -> error ;.
Lanalyseur avance alors jusquau prochain symbole correct aprs lerreur (ici le ;).
correction locale (cup2) : remplace le symbole lu par un autre qui
semble plus juste.
correction globale : trouve le plus petit ensemble dinsertions ou dltions de symboles tel que lanalyse soit correcte (mme si ces insertions
14

ou dltions ne sont pas lendroit mme de lerreur). Exemple : algorithme de Burke-Fisher -> pas dinsertions ou dltions au del de k
tokens avant lerreur, avec k fix.

3.4

Grammaires algbriques

Moyen de dcrire quels sont les flux de lexmes corrects et comment ils
doivent tre structurs. lanalyseur lexical dcrit comment former les mots du
langage. Lanalyseur syntaxique dcrit ensuite comment assembler les mots
pour former des phrases correctes.
La grammaire fournit une description des phrases (= programmes) syntaxiquement corrects.

3.4.1

Definition

G = {T, V, S, R}
T : alphabet des terminaux
V : alphabet des variables
S V : symbole de dpart ou axiome de la grammaire
R : ensemble fini de rgles. Chaque rgle r R est de la forme : X u ,
o X V et u (T V ) .
Une grammaire sert dcrire un langage. Pour engendrer un langage, on
part dun symbole particulier de la grammaire appel axiome, gnralement
not S.
On appelle drivation de u partir de A et on note A u, si u est obtenu
partir de A par lapplication squentielle dun nombre fini de rgles de G.
Langage engendr par la grammaire G : L(G) = {u T |S u}
Lanalyseur, connaissant la grammaire G :
reoit en entre un mot u sur lalphabet terminal, qui correspond au
programme source.
reconstitue une drivation de u, si le mot u est engendr par G.
indique une erreur syntaxique sinon.
Exemple : la grammaire "ET F " pour engendrer des expressions arithmtiques comme a + b * (c + d) :
E -> E + T
E -> T
15

T -> T * F
T -> F
F -> ( E )
F -> IDENTIFIER

3.4.2

Larbre de drivation

Il traduit lapplication des rgles dans une drivation de u partit de


laxiome S. La racine est laxiome, les noeuds interne sont les variables (ou
non-terminaux) de la grammaire, les feuilles sont les terminaux (correspondants aux lexmes fournis par lanalyseur lexical).
Remarque : larbre de drivation ne reflte pas lordre dapplication des
rgles : un mme arbre de drivation traduit une drivation gauche (o lon
applique toujours les rgles au non-terminal le plus gauche) et une drivation droite (o lon applique toujours les rgles au non-terminal le plus
droite).
Lanalyseur syntaxique tente de reconstituer un arbre de drivation pour le
mot (le programme) lu en entre. Sil commence par la racine de larbre, on
parle danalyse top-down, sil commence par les feuilles de larbre on parle
danalyse bottom-up.
Exemple : (mthode ascendante)
a + b * (c + d)
F + b * (c + d)
T + b * (c + d)
E + b * (c + d)
E + F * (c + d)
E + T * (c + d)
E + T * (F + d)
E + T * (T + d)
E + T * (E + d)
E + T * (E + F )
E + T * (E + T )
E + T * (E)
E+T*F
E+T
E

16

3.4.3

Ambigut

Une grammaire est ambige si il existe un mot u qui possde deux arbres
de drivation diffrents dans G, cest--dire sil existe deux structures grammaticales possibles donnant le mme mot.
Exemple : expressions arithmtiques :
E -> E + E
E -> E * E
E -> IDENTIFIER
Lanalyse pose problme si la grammaire est ambigue. On recherche alors
une autre grammaire engendrant le mme langage et qui soit non-ambige.
Exemple de la grammaire "ifthenelse" :
instr -> si expr alors instr
instr -> si expr alors instr sinon instr
Deux analyses possibles de la phrase :
si E1 alors S1 sinon si E2 alors S2 sinon S3
On prfre le alors le plus proche (p.201)
Grammaire quivalente :
instr -> instr_close
instr -> instr_non_close
instr_close -> si expr alors instr_close sinon instr_close
instr_non_close -> si expr alors instr_non_close
instr_non_close -> si expr alors instr_close sinon instr_non_close

3.5

Notions de grammaire attribue

Le rle de lanalyse syntaxique nest pas seulement didentifier les phrases


correctes dans la grammaire du langage. Cest aussi de collecter un certain
nombre dinformations utiles pour la phase suivante de la compilation : lanalyse smantique. Ces informations sont attaches aux symboles de la grammaire.
Chaque noeud de larbre syntaxique peut porter de linformation appele
attribut. Pour les feuilles de larbre, lanalyseur lexical peut fournir un at17

tribut associ un lexme donn (par exemple la valeur pour une constante
entire).
Pour calculer les attributs des noeuds interne de larbre, lanalyseur syntaxique associe chaque rgle de grammaire une action smantique. Cette
action indique comment calculer linformation attache un symbol de la
grammaire (ie : un noeud de larbre syntaxique) en fonction de linformation
attache aux autres symboles prsents dans la rgle.
On appelle traduction dirige par la syntaxe le fait dattacher de actions
smantiques aux rgles de la grammaire.
Une grammaire attribue est une grammaire dans laquelle :
chaque symbole, terminal ou non, peut avoir des attributs,
chaque attribut possde des rgles de calcul en fonction dautres attributs et de valeurs initiales,
chaque rgle de calcul est attache une rgle de la grammaire.
But : calculer les attributs pendant lanalyse syntaxique.
Principe :
LE
imprimer(E.val)
EE+T
E.val = E1.val + T.val
ET
E.val = T.val
TT*F
E.val = E1.val * T.val
TF
T.val = F.val
F(E)
F.val = E.val
F IDENTIFIER
F.val = IDENTIFIER.vallex
Soit la rgle calculant lattribut a associe la production A u :
a = f (a1 , ..., an )
a est synthtis si cest un attribut de A calcul en fonction des attributs
des symboles de u. Une grammaire est S-attribue si tous ses attributs sont
synthtiss. Linformation remonte dans larbre syntaxique.
Pour une grammaire S-attribue, le calcul des attributs se fait de faon mcanique si on a un analyseur LR (ex :CUP). Lors de la rduction par A u
on calcule le (ou les) attribut(s) de A en fonction des attributs des symboles
de u. On peut empiler les attributs calculs au cours de lanalyse. Les attributs des tokens sont initialiss par lanalyseur lexical.
Soit la rgle calculant lattribut a associe la production A u :
b = f (a1 , ..., an )
Lattribut b est hrit si b est un attribut dun symbole Xj de u, calcul en
fonction des attributs dautres symboles de u ou du symbole A.
18

Une grammaire est L-attribue si lattribut b du jme symbole Xj est calcul en utilisant seulement les attributs hrits de X1 , ..., Xj1 et celui de A.
Linformation descend dans larbre syntaxique.
Lutilisation des attributs permet une traduction dirige par la synatxe.
Cette technique est utilise par le compilateur pour effectuer le contrle
des types, lvaluation des expressions, la construction de reprsentations
intermdiaires.

3.6

Le logiciel CUP

Logiciel pour construire automatiquement un analyseur syntaxique.


Format dun source cup :
// imports de fichiers et paquetage
/* The grammar terminals */
terminal type_1 token_name1, token_name2, ...;
/* The grammar non terminals */
non terminal type_2 name1, name2, ...;
/* The grammar rules */
start with name1;
name1 ::=

...

{:

/*code Java */

:}

...
Le nom des terminaux de la grammaire doit tre donn dans le fichier CUP,
par exemple ID pour un identificateur. Il doit tre le mme que celui utilis par le lexer lors de lappel du constructeur de Symbol, par exemple
CalculetteSymbol.ID. Le logiciel CUP gnre automatiquement un fichier
CalculetteSymbol.java qui associe un code (i.e. une constante entire)
chaque terminal de la grammaire :
- vrifier quil y a autant de constantes dfinies dans ce fichier que de terminaux dclars dans cup par le mot-cl terminal,
- vrifier que pour chacun de ces terminaux, le lexer effectue un appel au
constructeur de Symbol (dans le fichier jflex).
Lanalyse syntaxique est donc ncessairement prcde dune analyse lexicale. Le constructeur du parseur prend donc en paramtre un analyseur
19

lexical (de la classe Lexer). La mthode appele pour effectuer lanalyse syntaxique sappelle parse().
Exemple de construction dun parseur :
public static void main(String[] args) {
FileReader myFile = new FileReader(args[0]);
Lexer myLex = new Lexer(myFile);
Parser myP = new Parser(myLex);
try {
result=myP.parse();
}
catch (Exception e) {
System.out.println("parse outor...");
}
}
Chaque fois que la mthode parse() a besoin dun nouveau terminal, elle
appelle la mthode de lanalyseur lexical yylex(). Lorsque yylex reconnat
un lexme, elle retourne au parseur une instance de la classe Symbol pour
dcrire le type de lexme rencontr.
Le symbole transmis peut aussi contenir des informations (du type Object)
sur le lexme reconnu, par exemple son nom ou sa valeur. Ces informations
constitue ce que lon appelle lattribut du symbole.

3.6.1

Les attributs dans jflex et CUP

Dans jflex, les lexmes ou tokens sont reprsents par des instances de la
classe Symbol. Les attributs dun token sont stocks dans une variable dinstance de cette classe. Le constructeur de Symbol, qui est appel lors de la
reconnaissance du token dans le flux dentre, peut prendre en paramtre
une valeur (appartenant une sous-classe de la classe Object) :
Symbol symbol (int type, Object value);
Cette valeur permet linitialisation de lattribut du token. Le token (instance
de Symbol) construit est alors transmis (par un "return") la fonction danalyse syntaxique.
Les non terminaux de la grammaire peuvent galement avoir des attributs. Lorsquun non terminal figure gauche dune rgle, son attribut est
20

automatiquement dsign par la variable RESU LT . Cette variable na pas


besoin dtre dclare, mais il faudra linitialiser dans le code java associ
la rgle.
Soit la rgle : F ID
. Pour le symbole F gauche, son attribut est not RESU LT
. Pour associer un attribut au symbole ID droite, il suffit de le nommer
(identificateur de votre choix) :
F ID : val
- lattribut de ID est alors val.
Soit la rgle : E E + T
. Pour le symbole E gauche, son attribut est not RESU LT
. Pour associer un attribut aux symboles droite, il suffit de les nommer :
E E : eval + T : tval
- lattribut du E droite est eval
- lattribut de T est tval
Si les attributs sont des nombres, on peut alors initialiser RESULT avec la
valeur eval + tval.
Il est galement possible de savoir quel portion de lentre est reconnue par le
non terminal T , par exemple. En effet deux autres variables sont associes
T en plus de son attribut tval, elles sont nommes automatiquement tvallef t
et tvalright (de type int) et indiquent deux positions dans le texte en entre.
On peut choisir un type pour lattribut associ chaque symbole de la grammaire :
terminal
type_1
token_name1;
non terminal type_2
name2;
Ces types doivent tre des classes Java.
Exemple :
- si le type associ expr est Integer :
expr ::= expr:eval PLUS term:tval
{: RESULT = new Integer(eval + tval); :}
- si le type associ ID est String :
factor ::= ID:t
{: System.out.println(" Attribut de ID: " + t ; :}

21

3.6.2

Priorits des oprateurs

Dfinir des priorits et associativits pour les lexmes permet dobtenir un


comportement autre que celui choisi par dfaut par cup en cas dambigit.
La priorit dun lexme est dtermine par la position de sa dfinition :
plus il est dfini tard, plus forte est sa priorit.
Un lexme est :
associatif gauche sil a t dfini par
precedence left OPERATEUR;
associatif droite sil a t dfini par
precedence right OPERATEUR;
On peut mettre plusieurs token dans une mme dclaration de precedence,
ils ont alors mme priorit.
La priorit dune rgle est celle de son terminal le plus droite. Si la rgle
na aucun terminal, sa priorit est plus basse. Il est aussi possible de modifier
la priorit dune rgle, par une directive %prec OPERATEUR place aprs la
rgle et ses actions. La rgle aura alors le mme priorit que OPERATEUR$
Exemple :
precedence right EGAL;
precedence left PLUS, MOINS;
On peut dduire de ces dfinitions que :
- les token PLUS et MOINS ont la mme priorit,
- le token EGAL a une priorit plus faible que les token PLUS et MOINS

3.7

Principe de lanalyse LR

Principe : construire larbre de bas en haut, en partant de la chane de


terminaux.
Objectifs :
deviner les rgles de grammaire conduisant une analyse correcte de
la chane de terminaux,
obtenir un algorithme dterministe,
pas de backtraking.
Ide : lanalyseur mmorise ( laide dune pile) la partie de larbre de drivation dj construite, et aussi ce quon doit arriver lire dans le futur pour
parvenir une analyse correcte.
Il peut ensuite comparer avec les symboles suivants sur le flux dentre :
22

lors dune analyse LR(k), il compare avec les k symboles suivants sur le flux
dentre.
Pour mmoriser ce qui a t analys et quelles sont les suites possibles de
lanalyse dj effectue, on utilise des rgles pointes (cest--dire des rgles
de grammaire dans lesquelles on rajoute un point en partie droite) appeles
items. Le point indique quelle portion de la rgle a dj t utilise pour
reconnatre lentre lue jusqu prsent.
Si une rgle peut scrire X uv o u et v sont deux mots, alors X u.v
est un item, avec le point entre u et v.
Lanalyseur alterne des tapes dempilement (en anglais shift), qui consistent
transfrer sur la pile le terminal lu sur le flux dentre, et des tapes de
rduction (en anglais reduce), qui consistent remplacer les items en sommets de pile par de nouveaux items rsultant de lapplication dune rgle de
grammaire (sans consommer le flux en entre). Les tapes de rduction permettent de "remonter" dans larbre de drivation. A chaque rduction, on
empile un nouvel ensemble ditems sur la pile. Ces nouveaux items traduisent
toutes les analyses futures possibles.
Exemple :
1. E -> E + T
E -> T
T -> T * F
T -> F
F -> ( E )
F -> IDENTIFIER
2. F -> IDENTIFIER
T -> F
E -> T
3. F -> ( E )
E -> E + T
E -> T
T -> T * F
T -> F
F -> ( E )
F -> IDENTIFIER

23

4. F -> ( E )
E -> E + T
...

3.7.1

Application dans Cup

Le logiciel CUP doit construit un automate dterministe. Dans chaque


tat, le symbole lu sur le flux dentre doit dterminer quel action (shift
ou reduce) effecter et dans quel nouvel tat se placer. Si la grammaire est
ambige, CUP ne pourra pas construire lautomate dterministe.
Il affichera alors :
- soit un conflit shift/reduce indiquant que deux actions diffrentes sont
possibles pour la lecture dun mme symbole en entre (consommation de
lentre ou application dune rgle de grammaire),
- ou un conflit reduce/reduce indiquant que deux rgles de grammaire
sappliquent, ce qui va correspondre deux futurs diffrents possibles dans
la suite de lanalyse.
Il faut alors modifier les rgles de grammaire pour supprimer toute ambigut.
Pour avoir le dtails de lautomate construuit, on peut utiliser les options
suivantes de CUP :
-dump_grammar
-dump_states
-dump_tables
-dump
Ces options produisent un affichage de la grammaire, des tats de lautomate
(utile pour reprer les conflits), des tables de transitions de lautomate. La
dernire option est identique lutilisation simiultane des trois prcdentes.

3.8

Algorithme dEarley

... A complter ...

3.9

Le traitement des erreurs de syntaxe

Lapproche choisie dans les analyseurs que nous construirons est la possibilit dutiliser une production derreurs.
Lorsque lanalyseur rencontre une erreur :

24

1. en labsence du symbole error dans les rgles de grammaire, il arrte


lanalyse.
Par contre si le symbole error figure dans les rgles :
2. il dpile les symboles analyss jusqu ce quil puisse empiler error, et
se place dans un mode spcial appel mode "reprise derreur",
3. il consomme en entre les symboles qui ont provoqu lerreur (comme
si lentre lue tait le symbole error),
4. si dans ce mode spcial, il rencontre une autre erreur avant davoir pu
empiler un certain nombre de symboles (par dfaut 3 dans CUP) il
revient en (2).
Attention : lintroduction du symbole error dans la grammaire peut crer
des conflits.
Si un nombre suffisant de tokens ont pu tre reconnus aprs le texte correspondant error, lanalyseur sort du mode "reprise derreur". Dans CUP ce
nombre suffisant de token est fix par la mthode error_sync_size() (qui
retourne 3 par dfaut). Le symbole error est un terminal spcial. Il na pas
besoin dtre dclar dans CUP.
Exemple :
statement :== expr SEMICOL | while_stmt SEMICOL | if_stmt SEMICOL | ... |
| error SEMICOL
{: System.err.println("syntax error before ;"); :}
Cette rgle indique lanalyseur, que si aucune des rgles normales de
statement ne sappliquent, une erreur de syntaxe doit tre signale et il
doit lire lentre jusquau prochain ; puis continuer comme si une instruction venait dtre reconnue, ce qui correspond une rduction par la rgle
instruction -> error SEMICOL.
On pourra connatre le texte correspondant au token spcial error en lui
ajoutant un attribut e et en utilisant les deux variables vues prcdemment
eleft et eright.
Du point de vue de lanalyse LR, si le parser rencontre un token qui
ne lui permet pas davancer sur un nouvel tat de lautomate, il revient
en arrire jusquau dernier tat rencontr contenant un item de la forme
X -> u.error v . Le parser passe alors dans ltat contenant litem
X -> u error .v comme sil avait lu le token "error" en entre et il consomme
tous les symboles sur le flux dentre jusqu ce quil puisse lire v.

25

Dautre mthodes du parser peuvent tre appele pour le traitement des


erreurs. Elles effectuent par dfaut un traitement de base. Pour un traitement plus fin, il sera ncessaire de les redfinir :
public void report_error(String message, Object info);
cette mthode doit tre appele pour signaler une erreur syntaxique. Par
dfaut le message est affiche sur la sortie derreur et le deuxime paramtre
est ignor.
public void report_fatal_error(String message, Object info);
cette mthode doit tre appele pour signaler une erreur non rcuprable.
Elle appele dabord la mthode report_error puis la mthode done_parsing,
qui arrte le parseur, et enfin elle lve une exception.
public void syntax_error(Symbol cur_token);
cette mthode est appele par le parseur chaque fois quune erreur est dtecte. Par dfaut elle appelle simplement report_error("Syntax error", null)
public void unrecovered_syntax_error(Symbol cur_token);
cette mthode est appele par le parseur chaque fois quil est incapable de
reprendre aprs une erreur. Par dfaut elle appelle simplement
report_fatal_error("Couldnt repair and continue parse", null)

26

Chapitre 4

Arbres de syntaxe abstraite


Larbre de drivation construit par lanalyseur syntaxique possde de
nombreux noeuds superflus, qui napportent pas dinformation sur la smantique du programme. La ncessit dutiliser une grammaire non-ambige
conduit souvent introduire des symboles auxiliaires dans la grammaire qui
ne seront pas utiles lanalyse smantique.
Pour faciliter la phase danalyse smantique il est donc souhaitable dutiliser une reprsentation plus synthtique et qui soit indpendante du langage
source.

4.1

Les Reprsentations Intermdiaires

Ncessit de construire une Reprsentation Intermdiaire, IR en abrg,


du code source qui synthtise toutes les informations ncessaires lanalyse
ultrieure et qui soit indpendante du langage source. Nous utiliserons une
IR sous forme darbres appels Arbres de Syntaxe Abstraite.
Un compilateur utilise en gnral plusieurs reprsentations intermdiaires au
cours de ses diffrentes phases, en allant de la plus structure, par exemple
les arbres de syntaxe abstraite, la plus simple, par exemple des listes dinstructions en pseudo-assembleur.
On utilise la traduction dirige par la syntaxe pour construire la premire Reprsentation Intermdiaire du programme, pendant lanalyse syntaxique du code source.

27

4.1.1

Diffrentes reprsentations du code source

1. arbre de drivation :
- li la grammaire
- utilis dans des compilateurs simple passe ou des interprteurs
- permet danalyser les codes syntaxiquement corrects
2. IR de haut niveau : arbre de syntaxe abstraite ou AST
- indpendant de la grammaire coeur du compilateur rutilisable
- utilis dans compilateurs multi-passes plusieurs parcours de lAST
- permet de grer les environnements et de contrler les types
- ne permet pas toutes les optimisations de code
3. IR de bas niveau : code trois adresses
- ne garde plus trace de la structure du programme source
- code linaire plus doptimisations (rorganisation du code, suppression dinstructions redondantes)
- permet le calcul du nombre de registres utiles

4.2
4.2.1

Reprsentation arborescente
Dfinition

Larbre de syntaxe abstraite est un arbre qui ne garde plus trace des
dtails de lanalyse syntaxique, cest--dire qui est indpendant de la grammaire, mais qui mmorise la structure du programme et les actions qui le
composent.
Ces arbres permettent de saffranchir des spcificits du langage source,
pour ne garder quune reprsentation gnrique du code source. Par exemple,
dans larbre de syntaxe abstraite, les symboles de ponctuation, la notation
spcifique des oprateurs algbriques auront disparu. Tout ce qui est dpendant du langage source doit tre limin de larbre de syntaxe abstraite. Un
mme arbre de syntaxe abstraite doit tre associ un mme programme,
quil soit crit en C, en python ou en Java.

28

if

variable: x (int)

==

thenelse

integer: 0

variable: y (int)

integer: 5

variable: y (int)

s0

4.2.2

Construction de larbre de syntaxe abstraite

limination des symboles de ponctuation, de tabulation,


limination de tokens propres au langage source (oprateurs spcifiques, mots-cl spcifiques,...)
prise en compte de la structure du programme (blocs, boucles,...)
plusieurs solutions sont possibles
Il existera plusieurs sortes de noeuds dans dAST : des noeuds "function",
des noeuds "variable", des noeuds pour diffrentes sorte de constantes (entires, relles, ...) des noeuds pour diffrentes sorte dinstruction (if, while,...)
des noeuds pour diffrentes sorte doprateur (egal, plus,...).
Larbre de syntaxe abstraite sera construit par lanalyseur syntaxique.
Les actions smantiques associes aux rgles de grammaires serviront donc
la construction des noeuds de larbre de syntaxe abstraite. Les informations
contenues dans un noeud de lAST seront entre autres des informations de
type, de valeur (pour les expressions), des chanes de caractres (pour les
identificateurs ou les oprateurs).
Lanalyse smantique sera effectue ensuite, par une succession de traitements appliqus aux arbres de syntaxe abstraite. Les traitement appliques
aux AST seront par exemple lvaluation, la vrification de type, la traduction dans une autre forme de reprsentation intermdiaire (code 3 adresses).

4.3

Implmentation

Dans le cas dun compilateur crit en java, deux approches sont possibles,
lapproche fonctionnelle ou lapproche objet.
29

integer: 7

4.3.1

Approche fonctionnelle

Dans un compilateur crit en Java, les noeuds de larbre de syntaxe abstraite seront implments laide de classes abstraites et de sous-classes :
- abstract class Statement, avec une sous-classe pour chaque alternative :
Block
If
While
...
- abstract class Expr, avec une sous-classe pour chaque alternative :
Plus
Times
Ident
...
Chaque traitement correspond une unique fonction qui reoit en paramtre
la classe de noeud traiter. En java on utilise le design pattern Visiteur
(interface Visitor). Il existera un visiteur pour valuer (classe Interpreter),
un visiteur pour vrifier les types (classe TypeVerificator, un visiteur pour
traduire en code intermdiaire (classe Traductor). Chaque visiteur doit surcharger sa mthode de visite pour quelle puisse prendre en paramtre toutes
les sous-classes de noeuds qui apparaissent dans lAST.

30

Inconvnient : trs grand nombre de sous-classes, donc trs grand nombre de


surcharges.

4.3.2

Approche Objet

La classe principale des noeuds de larbre de syntaxe abstraite sera la


classe ArbSynt.
Chaque sommet porte une tiquette appartenant un ensemble prdfini
dtiquettes (implmentes par la classe EnumTag en Java). On doit avoir
une tiquette pour indiquer une boucle, tiquette pour indiquer un identificateur, une tiquette pour chaque oprateur binaire, ...
Variables dinstance :
. etiquette
. fils droit, fils gauche
. type
. nom (String)
. valeur
Mthodes :
. affichage (toString)
. evaluation
. contrle de type
. traduction en code intermdiaire
On pourra spcialise la classe ArbSynt avec des sous-classes correspondant
aux diffrentes catgories de noeuds : Declaration, Instruction, Expression,...
On pourra alors utiliser lapproche objet pour implmenter les traitements : mthodes abstraites dans la super-classe, puis concrtes dans les
sous-classes. Chaque sous-classe peut implmenter des traitements qui lui
sont propres.

4.3.3

Extensibilit :

Ajout de nouveaux noeuds :


. plus facile avec lapproche objet : il suffit dajouter une classe,
. avec lapproche fonctionnelle : il faut modifier chaque fonction de traitement.

31

Ajout de nouveaux traitements :


. plus facile avec lapproche fonctionnelle : il suffit dajouter une nouvelle
fonction de traitement,
. avec lapproche objet : il faut rajouter le traitement dans chaque classe.

4.4

Arbre de syntaxe abstraite dcor

Larbre de syntaxe abstraite sera dcor avec des attributs :


Chaque sommet est dcor dun certain nombre dinformations (ses attributs)) : type, identificateur, valeur,... Ces informations sont mises jour au
cours de lanalyse syntaxique (traduction dirige par la syntaxe) en utilisant
les rgles de calcul dattributs.
Ces informations sont ensuite utilises pour les diffrents traitements appliqus larbre de syntaxe abstraite . Plusieurs parcours de lAST (parcours
en profondeurs) seront effectus dans les compilateurs multi-passes pour
les traitements suivants : affichage, valuation, vrification de types, optimisations, traduction en code intermdiaire (code trois adresses).

32

Chapitre 5

Analyse smantique : Tables de


Symboles, Environnements
Pour lvaluation, la vrification dun programme, il est ncessaire de mmoriser tous les identificateurs quil utilise.
Nous prendrons le cas dun langage de programmation tel que les programmes
comportent des parties dclarations et des parties excutables (C, C++,
JAVA). Les identificateurs (de variables, de fonctions, de classes) devront
tre dclars. Ils seront alors stocks dans des tables de symboles. Le compilateur utilisera les tables de symboles pour vrifier la cohrence des valeurs
et des types dans le programme et signaler les erreurs de smantique.

5.1

Tables de Symboles

La table de symboles est remplie pendant la compilation des parties contenant des dclarations :
dfinition de classes (en Prog. Objet),
dfinition de fonctions,
dclaration de variables.
La table de symboles est consulte pendant la compilation des parties excutables :
accs une instance de classe (en Prog. Objet),
appel de fonction,
accs une variable,
rfrence une variable (pointeurs).

33

La structure de donnes utilises pour reprsenter la table des symboles doit


tre performante, car le compilateur passe un temps important la consulter.
Les Tables de Symboles rassemblent toutes les informations utiles concernant
les variables et les fonctions ou classes du programme. Diffrentes tables
(relies entre elles) pourront tre associes aux diffrentes classes ou modules
du programme.
Pour toute variable, la table de symboles garde linformation de :
son nom,
son type,
son accs (adresse en mmoire ou offset dans le bloc dactivation dune
fonction),
sa porte.
Pour toute fonction, la table de symboles garde linformation de :
son nom
le nom et le type de ses arguments, ainsi que leur mode de passage
le type du rsultat quelle fournit
son code
son niveau dimbrication (level) dans les appels de fonction imbriqus,
sa porte.
La table des symboles est construite lors du parcours des parties contenant
des dclarations dans larbre de syntaxe abstraite. Chaque dclaration dun
nouvel identificateur ajoute une entre la table de symboles.
La table de symboles est consulte lors du parcours des parties contenant
des instructions dans larbre de syntaxe abstraite. Elle est utilise pour signaler toutes les erreurs concernant les dclarations de variables et lusage
des identificateurs :
variable initialise avant dtre dclare, variable lue en dehors de sa porte,
type attendu non conforme au type rel de la variable, dbordement de tableau,...
Exemple
int PGCD(int a , int b )
/* ajout de la fonction PGCD dans le table de symboles,
avec deux arguments int et type de retour int */
{
while ( b != a ) {
if ( a > b )
34

a = a-b;
else {
int tmp = a; /* ajout de la variable tmp dans la table de
symboles, avec le type int et la valeur a */
a = b;
b = tmp;
}
/* suppression de la variable tmp de la table de symbole */
}
return a;
}

5.2

Dclarations, Portes

Les types des identificateurs et des expressions se dduisent des dclarations


(de classes, de variables, de fonctions, ...). Les dclarations sont valides dans
leur porte.
Dfinition : On appelle porte dun identificateur la zone du programme
durant laquelle il est visible.
Les identificateurs ayant mme porte sont rassembls dans un mme ensemble.
Exemple :
{ int i=0;

/* dbut de la premire porte,

ajout de la variable i */

if (tab[i]==0){ /* deuxime porte */


int i;
/* nouvelle variable dans la deuxime porte */
i=readint();
...
} /* fin de la deuxime porte */
...
i++; /* incrmentation de la variable i de la premire porte */
...
} /* fin de la premire porte */
De mme que les blocs du programme peuvent tre emboits, les portes
aussi peuvent tre emboites. Des dclarations peuvent tre masques par
35

dautres dclarations effectues ultrieurement dans une autre porte (ou


mme parfois lintrieur de la mme porte).
La table de symbole doit reflter limbrication des portes. Cela signifie
donc que limplmentation des tables de symboles devra faire intervenir des
piles. La table de symboles devra permettre de retrouver chaque pas du
programme les variables de la porte courante et celles des portes englobantes.

5.3

Environnements

Les environnements sont des ensembles dassociation


identificateur descripteur,
o les descripteurs sont des ensembles dinformations associes aux identificateurs (nom, type, ...).
Les informations stockes dans les environnements sont collectes lors des
dclarations des identificateurs. La plus importante parmi ces informations
est le type de lidentificateur. Dans le cas o lidentificateur correspond une
variable qui a une valeur, on stokera galement la valeur (ou le moyen dy
accder en mmoire). Dans le cas dun tableau, on stockera aussi sa taille.
Dans le cas dun fonction, on stockera le nombre et le type de ses arguments.
Les environnements sont implments par les tables de symboles. Ce
sont des structures de donnes complexes faisant intervenir plusieurs tables
qui peuvent se rfrencer mutuellement. On peut avoir une table pour les
identificateurs des types nomms (exemple : typedef struct cellule*liste),
une autre table pour les identificateurs de fonctions, une troisime table pour
les identificateurs de variables de blocs... Pour compiler du code Java, on utilisera une table de symboles pour les variables dinstance dune classe et une
autre table de symboles pour les mthodes de la classe. La table des mthodes pourra contenir des pointeurs vers la table des variables dinstance.
Lors de la dclaration dun nouvel identificateur, on doit lajouter lenvironnement courant.
Lors de laffectation dune valeur un identificateur, on doit le chercher
dans lenvironnement courant :
- sil nest pas prsent, on doit signaler une erreur,
- sil est prsent, on ajoute la valeur au descripteur de lidentificateur.
Lors de la rfrence un identificateur, on doit le chercher dans lenvironnement courant :

36

- sil nest pas prsent, on doit signaler une erreur,


- sil na pas t initialis, on doit signaler une erreur,
- sinon on retourne sa valeur dans lenvironnement courant.

5.4

Implmentation dune table de symbole

Considrons une table de symboles pour la fonction principale du programme. Plusieurs implmentations sont possibles. Le choix de la structure
de donnes est important pour la rapidit du compilateur.
On associe la fonction principalke du programme source une pile de
portes. Lorsque lon sort dune porte, on doit dpiler tous les symboles
qui ne peuvent plus tre utiliss. Lopration "dpiler" doit tre facile implmenter. La recherche dun identificateur (opration frquemment utilise
pour la compilation des instructions) doit tre efficace.

5.4.1

Une pile de tables de hachage :

On empile une nouvelle table de hachage chaque fois que lon rentre dans
une nouvelle porte.
Avantage : gestion facile des portes.
Inconvnient : la recherche dun identificateur se fait en parcourant toutes
les portes de la pile, elle peut donc tre trs lente, sil nest pas dans la
porte courante. Accs au pire en O(n) pour une table de n identificateurs.

5.4.2

Une table de hachage contenant des piles :

Chaque cl de la table correspond un identificateur auquel est associ une


pile de descripteurs. Les informations sur lidentificateur sempilent, de la
porte la plus externe la porte la plus interne. Le sommet de pile correspond la dfinition la plus rcente de lidentificateur. Lorsque lon sort
dun bloc, on doit effacer tous les identificateurs qui ne sont plus valables.
Ncssit de reprer les identificateurs appartenant une mme porte, par
exemple en ajoutant dans leurs descripteurs un numro de porte.
Avantage : recherche rapide (en temps constant) dun identificateur.
Inconvnient : ncessit de retrouver les identificateurs dune mme porte
lorsque lon sort dun bloc. Il faut parcourir toutes les entres de la table,
donc la suppression peut tre lente. Suppression au pire en O(n) pour une

37

table de n identificateurs.
Pour palier cet inconvnient, on peut rajouter une pile de portes, qui
pointe sur toutes les dclarations dune mme porte.
Inconvnient : place plus importante en mmoire.

5.4.3

Une pile darbres de recherche persistants :

On empile un nouvel arbre de recherche, chaque fois que lon rentre dans
une nouvelle porte. Chaque arbre empil reprsente une porte ; larbre possde des pointeurs vers les identificateurs des portes englobantes qui nont
pas t redfinis.
Avantage : recherche et ajout en temps moyen O(log(n)).
Implmentation des arbres persistants :
fonction ajouter( x : nom, t : type, u : Arbre ) : Arbre
dbut
si ( u==NULL)
retourner new Arbre( x, t, NULL, NULL ) ;
38

s3

s2

s0

G: int

G: int

G: int

right
Z: int
left
X: int

left

right

B: string
left
A: string

left

right

B: string
right

Z: array [100] OF int

right

left

C: int

left
B: string
left

A: array [12] OF array [10] OF pointer OF string

right
Y: int

sinon si ( x < u.nom)


retourner new Arbre(u.nom, u.type,
ajouter( x, t, u.gauche), u.droit) ;
sinon si ( x > u.nom)
retourner new Arbre(u.nom, u.type, u.gauche,
ajouter(x, t, u.droit)) ;
sinon si ( x==u.nom)
retourner new Arbre(u.nom, u.type, u.gauche, u.droit) ;
fin

Exemple : Soit le bloc de code suivant :


{

/* dbut de porte s0 */
var
G: int;
B: string;
Z: array [100] of int;
A: array [12] of array [10] of pointer of string;

39

if (A<5) then { /* dbut de porte s1 */


var
T: int;
T=-3;
B=B+"azertyuiop";
} /* fin de porte s1 */
else { /* dbut de porte s2 */
var
C: int;
X: int;
Z: int;
Y: int;
X=0;
while (X < 100) do { /* dbut de porte s3 */
var
A: string;
X=X+1;
A=A+"qsdfghjklm";
} /* fin de porte s3 */
} /* fin de porte s2 */
A = 8;
}

40

Chapitre 6

Types, vrification de type


6.1

Introduction

Les types des sous-expressions conditionnent le rsultat dune expression.


Par exemple lexpression "20"+10 peut donner 30 ou 2010 ou encore une
erreur. Le typage va permettre dune part de rejeter les programmes absurdes, dautre part deffectuer une traduction correcte des programmes nonabsurdes en langage machine.
Chaque langage possde son systme de typage. En se rfrant ce systme
de typage, le compilateur peut contrler que chaque utilisation de variable ou
de fonction est conforme au type attendu. Le compilateur peut aussi infrer
le type dune expression, cest--dire dduire son type daprs le contexte.

6.2

Typage statique, typage dynamique

Si le type dune variable ou dune expression est dtermin lors de la compilation : on parle de typage statique. Cest le coeur du compilateur qui a pour
rle de dterminer les types. Cela peut tre fait au moment de la cration
de larbre de syntaxe abstraite. Lavantage est alors quil ny aura pas de
cration darbre si le typage est incorrect.
Si le type dune variable est dtermin lors de lexcution : on parle de typage
dynamique. Certains contrles, comme le fait que les indices dun tableau
soient corrects, cest--dire ne dpassent pas les bornes, ne peuvent tre faits
que dynamiquement.
En java, par exemple, on a un mlange de contrle statique et de contrle
41

dynamique. On dira quun systme de typage est sain, sil assure que toutes
les valeurs utilises lexcution sont bien du type dtermin statiquement.
Sil y a une relation dordre entre les types, il faut que les valeurs lexcution soit des sous-types des types dtermins la compilation.
Un langage typage statique peut rejeter des programmes qui aurait qui
aurait pu tre excuts sans erreur :
if (test)
return 42 + 75;
else
return 42 + "ab";
Si le test est toujours faux, le programme peut sexcuter.
Mais si le contrle de type est statique, il est rejet.
Un langage typage dynamique implique de faire des tests unitaires, qui
testent les excutions possibles du programme. Mais il est impossible est
trop coteux de faire des tests exhaustifs.

6.3

Sret de typage

Dfinition La sret de typage est la capacit du langage rejeter les programmes absurdes.
Un langage est sr ou fortement typ si tout typage incorrect entrane la
violation dune rgle de typage et donc provoque une erreur ( la compilation
ou lxecution).
langage fortement typ programmes sans erreur de type.
A contrario, un langage est faiblement typ si le comportement du programme nest plus spcifi en cas de typage incorrect.
Remarque : il existe diffrentes dfinitions de fortement typ. Pour certains, un langage est fortement typ si toutes les variables sont types et
aucune conversion implicite de type nest possible.
Attention : il ny a pas de lien entre le fait que le typage soit statique ou
dynamique et le fait que le langage soit fortement ou faiblement typ.
- langage C : typage static / faiblement typ
- langage PhP, JavaScript : typage dynamique / faiblement typ
42

- langages ML, Haskell : typage static / fortement typs


- langages Python, Lisp : typage dynamique / fortement typs
Exemples de langages faiblement typs :
var x:= 5;
var y:="37";
var z:= x+y;
- en VisualBasic : 37 -> (int)37, donc z vaut 42
- en JavaScript : 5 -> 5, donc z vaut 537
Que se passerait-t-il si y = ab ? En JavaScript : z vaut N aN de type
number.
int x= 5;
char y[]="37";
char *z=x+y;
- en C : z pointe 5 caractres aprs le dbut de y, donc le rsultat est indfini.
Le risque en essayant davoir des langages trs fortement typs est de rejeter
galement un certain nombre de programmes non-absurdes. Le but recherch
est davoir des langages relativement fortement typs, tout en restant assez
expressifs, cest--dire ne rejetant pas trop de programme corrects.

6.4

Reprsentation des types

Le contrleur de types utilise un systme de typage. Cest la donne de


deux choses :
- une reprsentation des types (types de bases et types complexes),
- un ensemble de rgles de typage, premettant de savoir si les expressions
sont bien types.
le contrleur utilise le systme de typage pour contrler les types lors dune
instruction, ou pour infrer (dduire) le type dune expression.

6.4.1

Construction des types

On construit des types complexes en appliquant des constructeurs de types


des types simples (types de bases et types nomms).

43

1. types de base (boolen, caractre, entier, rel), void, error,


2. types nomms introduits par le programmeur (typedef, classes),
3. constructeurs de types :
tableaux : array(I, T ) o I est un ensemble dindices
produits de deux types : T 1 T 2
structures avec champs : struct( name1 T 1, name2 T 2)
pointeurs vers un type : pointeur(T 1)
fonctions : T 1 T 2
Pour implmenter les types gnriques (comme dans les classes gnriques
en Java par exemple), on rajoute :
4. des variables de type : 0 a (un type 0 a inconnu).

6.4.2

Expressions de types

Un type complexe est dcrit par une expression de type faisant intervenir des
constructeurs de types et des types de base.
Exemple : char char pointeur(int)
On peut reprsenter une expression de type par un graphe. Le graphe est un
arbre si lexpression nutilise pas de types rcursifs.
Types dun langage pseudo C Les types dun langage pseudo C peuvent
tre construits en utilisant les rgles de grammaire suivantes :
Prog ->
Declist
Decl ->
Type ->
Expr ->

Decls Expr
-> Decl Declist | Decl
id : Type | Type id (Type)
char | int | float | Type[] | Type*
littral | integer | real | Expr(Expr) | Expr[Expr] | *Expr
| Expr PLUS Expr |...
44

Le contrleur doit procder des vrifications de type dans les expressions.


Il doit sassurer que lusage dun identificateur de variable ou de fonction est
conforme son type dclar.
- Lors de la dclaration dune variable, il faut construire une reprsentation
de son type (arbre) et la stocker dans lenvironnement courant.
- Lors de lutilisation dune variable dans une expression, il faut comparer le
type dclar pour la variable et le type attendu par lexpression, et dterminer sils sont quivalents.
Pour cela on a besoin de savoir quand est-ce que deux expressions de type
sont quivalentes.

6.5

Equivalence des expressions de type

Dans quelle mesure peut-on dire que deux types sont quivalents ?
Deux types sont quivalents sils sont construits laide du mme constructeur de type (tableau, pointeur,...) avec des paramtres qui sont eux-mmes
des types quivalents.
Solution simple :
fonction Equiv ( s , t ) : boolen
dbut
si s et t sont le mme type de base alors
retourner vrai
sinon si s = tableau( s1, s2) et t = tableau( t1, t2)
alors
retourner Equiv( s1, s2) et Equiv( t1, t2)
sinon si (s = s1 x s2 ) et (t = t1 x t2 )
alors
retourner Equiv( s1, s2) et Equiv( t1, t2)
. . .
sinon
retourner faux
fin
Attention : des cycles peuvent apparatre dans la construction des types,
ce qui pose problme pour crire une fonction dquivalence rcursive.
Exemple :
type lien = pointer of cellule ;
45

type cellule = structure {


info : int ;
suivant : lien ;
}
Dans ce cas il faut introduire un type nomm et ramen lquivalence des
sous-types nomms lgalit des noms.

6.6

Coercition, surcharge et polymorphisme

Problme : une mme constante peut reprsenter des types diffrents en machine. Un mme symbole doprateur ou fonction peut reprsenter diffrnets
code excutables en machine.
Donc le type associ une expression peut tre diffrent selon le contexte.
Et un identificateur peut correspondre non pas un seul type, mais un
ensemble de types.

6.6.1

Cohercition

Definition : Coercition : conversion implicite (par le compilateur) dun oprande dans le type attendu par lexpression.

Cela implique une relation dordre entre les types. Le compilateur convertira
vers un type suprieur. Dans le cas dune conversion dans lautre sens (cest-dire vers un type infrieur) il faut que le programmeur utilise un mcanisme
de conversion explicite appel cast.
Exemple :
{ a : int ;
b : real ;
x : int ;
y : real ;
x = a + b;
y = a + b; }
Infrence de types laide des attributs :
E -> E :e1 op E :e2 {: ...
if ( e1.getType() == INT && e2.getType() == INT )
RESULT.putType( INT );
46

else if ( e1.getType() == FLOAT || e2.getType() == FLOAT )


RESULT.putType( FLOAT );
else RESULT.putType( ERROR );
:}
Instr -> LeftExpr :e1 ASSIGN Expr :e2 {: ...
if ( e2.getType() == INT && e1.getType() == FLOAT )
RESULT.translate();
RESULT.getCode().getLeft().RealToInt();
else if e2.getType() == FLOAT && e1.getType() == INT
RESULT.translate();
RESULT.getCode().getRight().RealToInt();
:}
E -> ID :id {: ...
RESULT.putType( currentEnv.find(id) );
:}
E -> E :e1 (E :e2) {: ...
RESULT.putType( currentEnv.find(e1).getRight() );
{t|s, s e2.types&&s > t e1.types}
:}

6.6.2

Surcharge

Definition Surcharge doprateur ou de fonction : un nom de fonction ou


doprateur est surcharg, sil correspond selon le contexte plusieurs implmentations (codes machine) diffrentes.

Dans ce cas, plusieurs expressions de type vont tre associes au mme oprateur ou la mme fonction. Donc les attributs des noms doprateurs ou
des noms de fonctions seront des ensembles de types. Pour contler le type
dune expression, il sagira alors de faire des parcours de larbre de syntaxe
abstraite de lexpression, jusquau moment o lon pourra selectionn par
limination un type unique dans lensemble.
Exemple :
Loprateur daddition peut reprsenter selon le contexte une addition dentiers, de rels, de complexes, ou une concatnation de chaines.

47

Problme :
function f (i, j : int) : real ;
int int -> real
function f (i, j : int) : int ;
int int -> int
Quel est le type de f (4, 5) ? ? ? En particulier dand le cas suivant :
i : i n t;
x : r e a l;
f oo(4, 5) + x ; real
f oo(4, 5) + i ;
int

6.6.3

Polymorphisme

Definition Une fonction ou un oprateur est polymorphe si son code peut


tre excuter avec des arguments de types diffrents.

Contrairement la surcharge, on a donc ici un seul code machine, quelque


soit le type darguments.
Pour reprsenter une fonction ou un oprateur polymorphe, on utilisera des
variables de type.
Exemples :
Oprateur & en C
Si X est de type T , alors &X est de type pointeur vers T .
Oprateur [ ] en C
Si X est de type tableau(T, I) et k est de type entier, alors X[k] est de type
T.

On peut dvelopper des fonctions polymorphes pour son propre compte.


Exemple : longueur dune liste
# letrec long = function
[ ] -> 0
| t ::q -> 1 + long(q) ;
long : list a -> int
Les types dun langage polymorphes peuvent tre construits en utilisant les
rgles de grammaire suivantes :
P -> D ; E
48

D -> D ; D
| id : Q
Q -> id. Q
| T
T -> T T
| T | T T | pointeur(T ) | liste(T ) |type | ID | (T )
E -> E(E) | E, E | ID
Exemple 1 :
deref : x . pointeur(x) x
Vrifier le type :
q : pointeur ( pointeur ( entier ) )
deref ( deref ( q ) )
pointeur(y) = pointeur(pointeur(entier))
ppcu = {(y, pointeur(entier))}
pointeur(x) = pointeur(entier)
ppcu = {( x, entier)}
Exemple 2 :
first : x, y . x y x
+ : int int int
Infrer le type de :
f un : p (+(f irstp)1)

p: x X y
(first p) :
1 : int

(first p)+1 : int


x : int
fun : int X y -> int
donc on trouve f un : y . int y x

49

Chapitre 7

Gnration de code
intermdiaire
Le coeur du compilateur, aprs avoir vrifier les portes et les types,
doit traduire lArbre de Syntaxe Abstraite en Code Intermdiaire. Ensuite
le Back-End du compilateur traduit le Code Intermdiaire en Assembleur.

7.1

Choix dune Reprsentation Intermdiaire

Pour traduire efficacement lArbre de Syntaxe Abstraite, on a besoin


dune Reprsentation Intermdiaire (IR) assez expressive afin dexprimer
toutes les constructions syntaxiques.
Pour facilit le travail du Back-End, on a besoin dune Reprsentation Intermdiaire pas trop loigne des machines relles, afin didentifier clairement
les instructions de lAssembleur.
On est donc amener utiliser successivement plusieurs Reprsentations Intermdiaires, dun niveau dabstraction assez lev au dpart, vers un niveau
dabstraction beaucoup plus bas au final.
Les Reprsentations Intermdiaires comportent en gnral :
des oprateurs arithmtiques,
des oprateurs logiques,
des tiquettes, des sauts (conditionnels ou non) vers des tiquettes,
des accs mmoire (en lecture ou en criture),
des dplacements de donnes dun registre dans un autre,
des appels de fonctions.
Les critres de choix dune Reprsentation Intermdiaire sont
la facilit de gnration,
50

la facilit de manipulation,
la taille du code produit,
le pouvoir dexpression.
On commencera par une Reprsentation Intermdiaire structure sous
forme darbres ou de dags (sauts), pour aboutir une Reprsentation
Intermdiaire linaire (pseudo-code assembleur pour machine abstraite).
Paralllement ces Reprsentations Intermdiaires du code, on utilise en
gnral un graphe de flot de contrle, refltant les excutions possibles du
programme et permettant diverses optimisations.

7.2

Arbres de Code Intermdiaire

Un arbre de Code Intermdiaire comportera des noeuds qui peuvent tre


de type Stm (pour les instructions) ou de type Exp (pour les expressions).
A cela sajoute des noeuds de type Label pour tiqueter difrents points
du programme et des noeuds de type Temp pour reprsenter les registres de
calcul.
Les noeuds de type Exp (sous-classes) sont les suivants :
CONST
TEMP
NAME
BINOP
MEM
CALL
ESEQ

(int value)
(Temp temp)
(Label label)

// pour un nom de fonction, son tiquette indique


// ladresse mmoire de la chane de caratre
(EnumOp binop, Exp left, Exp Right)
(Exp exp)
// accs en lect./ecrit. selon contexte
(Exp func, Explist args)
(Stm stm, Exp exp) // instructions ayant une valeur associe

Les noeuds de type Stm (sous-classes) sont les suivants :


MOVE
EXP
JUMP
CJUMP
SEQ
LABEL

(Exp dst, Exp src)


// ecriture dans un registre ou en mmoire
(Exp exp)
// converti exp en Stm
(Exp label)
(EnumRel relop, Exp left, Exp right, Label iftrue, Label iffalse)
(Stm left, Stm right) // liste dinstructions
(Label label)
// pose une tiduqette

Exemple 1. Une expression a = x + 2 y sera traduite en :

51

SEQ

CJUMP

EQ

TEMP

CONST

t0

SEQ

L0

L1

LABEL

SEQ

L0

MOVE

SEQ

TEMP

CONST

JUMP

t1

SEQ

NAME

LABEL

SEQ

L2

L1

MOVE

TEMP

CONST

t1

LABEL

L2

BINOP( PLUS, TEMP(x),


ESEQ(
MOVE( TEMP(t0), BINOP( MULT, CONST(2), TEMP(y))),
TEMP(t0)))
Exemple 2. Une expression a[e] sera traduite en
MEM ( BINOP ( PLUS , MEM( CodeInterm(a)) , WORDMUL( CodeInterm(e))))
o CodeInterm(a) (resp. CodeInterm(e)) dsigne larbre de code intermdiaire correspondant lexpression a (resp. e), et W ORDM U L dsigne la
multiplication (de e) par une constante reprsentant la taille dun lment
de la pile utilise pour limplmentation du compilateur.
Exemple 3. Une instruction conditionnelle : if(t0 == 0) then t1 = 5; else
t1 = 7; est traduite par des sauts avec trois tidquettes :
SEQ( CJUMP ( EQ , TEMP(t1) , CONST(0) , L0 , L1 ),
SEQ( LABEL ( L0 ),
SEQ( MOVE( TEMP(t1), CONST(5)),
SEQ( JUMP ( L2 ),
SEQ( LABEL ( L1 ),
SEQ( MOVE( TEMP(t1), CONST(7)),
SEQ( LABEL( L2 ))))))))
Exemple 4. Une dclaration

52

typedef int arrtype[3];


arrtype array1 = { 0, 0, 0 };
est traduite en
ESEQ (
SEQ( MOVE ( TEMP t, CALL ( malloc , WORDMUL( CONST(3)))),
SEQ( MOVE ( MEM ( BINOP ( PLUS, TEMP t, ( WORDMUL( CONST(0)), CONST 0)))),
SEQ( MOVE ( MEM ( BINOP ( PLUS, TEMP t, (WORDMUL( CONST(1)), CONST 0)))),
MOVE( MEM ( BINOP ( PLUS, TEMP t, ( WORDMUL( CONST(2)), CONST 0))))))),
TEMP t )
Remarque :
- Ladressage mmoire est explicit :
lecture : MOVE( t, MEM(a)) ,
criture : MOVE( MEM(a), t).
- Lappel de fonction se distingue des autres expressions :
MOVE( t, CALL( NAME(f), e)).

7.2.1

Arbres canoniques

But : linarisation du code.


Larbre de Code Intermdiaire doit tre rcrit sous la forme dune liste
darbres canoniques sans ESEQ qui pourra ensuite tre traduite en Code trois
adresses.
1. Faire remonter les noeuds ESEQ vers la racine de larbre de Code Intermdiaire, afin de les liminer.
Attention : les ESEQ peuvent faire des effets de bord si les sousexpressions sont evalues dans un ordre diffrent, cela peut donner des
rsultats diffrents.
2. Dplacer les appels de fonctions pour quils figurent en tte des expressions : EXP(CALL(f,...)) ou MOVE(TEMP t, CALL(f,...)).
3. Dans la plupart des machines, pour les tests conditionnels, si le test
est faux on continue linstruction suivante -> rorganiser les CJUMP
de sorte quils soient immdiatement suivis par la partie excute si
le test est faux.
Exemple 1 : t1 = x + 2 y
53

MOVE( t1, BINOP( PLUS, TEMP(x),


ESEQ( MOVE( TEMP(t0), BINOP( MULT, CONST(2), TEMP(y))),
TEMP(t0))))
Cet arbre sera rcrit en :
MOVE( TEMP(t1), ESEQ( MOVE( TEMP(t0), BINOP( MULT, CONST(2), TEMP(y))),
BINOP( PLUS, TEMP(x), TEMP(t0) )))
Puis rcrit en :
SEQ( MOVE( TEMP(t0), BINOP( MULT, CONST(2), TEMP(y))),
MOVE( TEMP(t1), BINOP( PLUS, TEMP(x), TEMP(t0) )))
Puis traduit en Code 3 adresses :
t0 = 2 * y;
t1 = x + t0;
Exemple 2 : a = f (x + 2 y)
MOVE( TEMP(a),
CALL(f, ESEQ( SEQ( MOVE( TEMP(t0), BINOP( MULT, CONST(2), TEMP(y))),
MOVE( TEMP(t1), BINOP( PLUS, TEMP(x), TEMP(t0) ))),
TEMP(t1))))
Cet arbre sera rcrit en :
MOVE( TEMP(a),
ESEQ( SEQ( MOVE( TEMP(t0), BINOP( MULT, CONST(2), TEMP(y))),
MOVE( TEMP(t1), BINOP( PLUS, TEMP(x), TEMP(t0) ))),
CALL(f, TEMP(t1))))
Puis rcrit en :
SEQ( MOVE( TEMP(t0), BINOP( MULT, CONST(2), TEMP(y))),
SEQ( MOVE( TEMP(t1), BINOP( PLUS, TEMP(x), TEMP(t0) ))
MOVE( TEMP(a), CALL(f, TEMP(t1)))))
Puis traduit en Code 3 adresses :
t0 = 2 * y;
t1 = x + t0;
a = call( f, t1);

54

Principales transformations, avec s et e1 qui commutent :


le code intermdiaire :
est remplac par :
MEM(ESEQ(s,e))

ESEQ(s,MEM(e))

MOVE(TEMP t, ESEQ(s,e))

SEQ(s, MOVE(TEMP t,e))

BINOP(op, ESEQ(s, e), e1)

ESEQ(s, BINOP(op, e, e1))

CJUMP(op, ESEQ(s, e), e1, L1, L2)

SEQ(s , CJUMP(op, e, e1, L1, L2))

BINOP(op, e1, ESEQ(s, e))

ESEQ(s, BINOP(op, e1, e))

CJUMP(op, e1, ESEQ(s, e), L1, L2)

SEQ(s , CJUMP(op, e1, e, L1, L2))

Si s et e1 ne commutent pas :
le code intermdiaire :

est remplac par :

BINOP(op, e1, ESEQ(s, e))

ESEQ(MOVE(TEMP t, e1),
ESEQ(s, BINOP(op, TEMP t, e)))

CJUMP(op, e1, ESEQ(s, e), L1, L2)

SEQ(MOVE(TEMP t, e1),
SEQ(s, CJUMP(op, TEMP t, e, L1, L2)))

7.3

Code trois adresses

Assembleur de haut niveau comportant des sauts, des tiquettes, et o chaque


oprateur a au plus deux oprandes.
On regroupe dans des blocs basics les parties de code qui ne contiennent ni
saut ni label -> ncessairement excuts squentiellement, sans interruptions.
Exemple 1. En supposant a, x et y dj dans des registres, une instruction
a = x + 2 y + 1; sera traduite en :
t0 = 2 * y ;
t1 = x + t0 ;
a = t1 + 1 ;

55

Exemple 2. Une instruction x = a[i]; sera traduite en


t0 = a;
t1 = w * i; // w est la taille dun lment de pile
x = t0 + t1;
Exemple 3. Une instruction conditionnelle if (t0! = 0) then t1 = 5; else
t1 = 7; sera traduite en :
Cjump( NEQ, t0, 0, L1, L2);
L1:
t1 = 5;
L2:
t1 = 7;
Remarque :
- On obtient un mlange de code squentiel et de sauts,
- On suppose lors de cette traduction, que lon dispose dun nombre infini
de registres.
On utilisera donc un processeur squentiel avec des registres et des accs
mmoire.
- Le calcul du nombre de registres ncessaires se fera plus tard,
- Lutilisation effective des registres du processeur se fera plus tard.
- La traduction des appel de fonctions se fera plus tard(dpend du processeur
cible).

56

Chapitre 8

Du code intermdiaire vers le


code optimis
Loptimalit du code est indcidable, puisque le problme de savoir si
deux programmes ont le mme comportement (terminent ou non) sur toutes
les entres est indcidable.
Plusieurs sortes doptimisations :
optimiations locales : lintrieur des blocs
optimiations globales : sur lensemble du graphes de flot de contrle,
cest --dire sur lensemble des blocs.
Il existe aussi des optimisations dpendantes du code cible, qui seront faites
par le back-end.

8.1

Optimiations locales

Elimination de sous-expressions communes.


Simplifications arithmtiques (x = x + 0;).
Reduction de force (remplacer des oprations par dautres moins coteuses).
Propagation des constantes (a = 2; x = y + a;)
Elimination de code mort (code inutile).
Propagation des copies.

8.2

Optimiations globales

Dplacement de code (code invariant hors des boucles, changes de


boucles).
57

Elimination globale de code mort.


Propagation globale des constantes.
Elimination des redondances (calcul de valeurs dj connues).

8.3

Ordre des optimisations

Lordre dans lequel on effectue les optimisations est important car elles interagissent.
Exemple :
x=5;
...
if (x<10) then ... // propagation des cstes -> elimination code mort

Propagation des constantes

Simplifications arithmtiques Elimination de code mort

Propagation locales et globales des copies

Elimination de sous-expressions communes Elimination de code mort

Code invariant hors des boucles Elimination de code mort

Reduction de force

Echanges de boucles, fusion ou redistribution de boucles

8.4

Analyse du flot de donnes

Il sagit dun raisonnement statique ( la compilation) sur des flots dynamiques de donnes (qui seront obtenus lexcution). Lanalyse statique
permet dobtenir des informations qui sont ensuite utilises pour optimiser
le programme. Le programme est dabord reprsent sous forme dun graphe,
le graphe de flot de contrle, indpendant du langage source ou du langage cible du compilateur.
Les questions auxquelles doit rpondre lanalyse du flot de donnes sont :
58

Quelles affectations de variables "atteignent" un certain point du programme (i.e. sont encore valides en ce point, quelque soit lexcution) ?
Quelles variables sont lues/modifies dans un bloc donn ?
Est-ce que une occurrence de la variable x est la dernire du programme ?
Lanalyse du graphe de flot de donnes consiste :
crire un systme dquations dont les inconnues sont des ensembles
de variables ou des ensembles dexpressions du programme,
rsoudre ces quations en recherchant le plus petit ou plus grand point
fixe (programmation dynamique).
Les solutions de ces quations sont ensuite utilises pour en dduire des optimisations (suppression de code mort, de code redondant,...).
Lanalyse peut se faire de deux faons :
- Analyse avant : on dtermine les proprits dun bloc en fonction de ses
prdecesseurs,
- Analyse arrire : on dtermine les proprits dun bloc en fonction de ses
succsseurs.

8.4.1

Graphe de flot de contrle

Les sommets du graphe de flot de contrle sont des blocs dinstructions et les arcs reprsentent le flot des donnes au cours des diffrentes
excutions possibles.
Dcoupage du code linaris en blocs :
Toute tiquette marque le dbut dun bloc. Tout saut ou branchement marque
la fin dun bloc. Toute instruction suivant un branchement ou un saut marque
le dbut dun autre bloc.
Un arc relie deux sommets du graphe de contrle si le contrle peut passer du premier sommet au deuxime au cours dune excution particulire
du programme.
Exemple :
B1 :
a = 0
B2 :
LABEL L1

59

b = a+1
c=c+b
a=a *2
if a<n goto L1 else goto L2
B3 :
LABEL L2
print c
Les arcs sont :
B1 -> B2
B2 -> B2
B2 -> B3

8.4.2

Analyse des variables vivantes

Une variable est vivante la sortie dun bloc B si elle est utilise par un bloc
que lon peut atteindre depuis B.
On notera succ(B) lensemble des blocs successeurs du bloc B dans le graphe
de flot de contrle. Il sagira dune analyse arrire, car le calcul des ensembles
de variables pour un bloc B se fera en fonction des valeurs obtenues pour les
successeurs de B.
On dfinit dabord quatre ensembles de variables.
Out(B) : variables vivantes en sortie de B
In(B) : variables vivantes en entre de B
Def (B) : variables qui figurent dans un membre gauche dans B
U se(B) : variables qui figurent dans un membre droit dans B avant dtre
dfinies (i.e. de figurer dans un membre gauche)
Les quations reliant ces ensembles sont les suivantes :
In(B) = U se(B) (Out(B) Def (B))
Out(B) = B 0 Succ(B) In(B 0 )

Algo :
1. Dterminer U se et Def pour chaque bloc.

60

2. Initialiser Out (et In) pour chaque bloc.


3. Appliquer les quations -> nouvelles valeurs des ensembles In et Out.
4. Tant que In et Out augmentent, retourner en (3).
Lalgorithme termine puisque lensemble des variables du programme est fini.
On effectue i boucles de calcul. Si Outi (b) dsigne lensemble Out(B) calcul
lors de la ime boucle, on a Out0 (B) = . A la boucle i + 1, on a :
Ini+1 (B) = U se(B) (Outi (B) Def (B))
Outi+1 (B) = B 0 Succ(B) Ini+1 (B 0 )
Exemple
L1 :
t1=a
L2 :
t2=b
t3=a+b
if (t1 > t2) goto L3 else goto L4
L3 :
t2=t2+b
t3=t3+c
if (t2 > t3) goto L5 else goto L2
L4 :
t4=t3 * t3
t1=t4+t3
goto L1
L5 :
print(t1, t2, t 3)

8.4.3

Optimisation

- Allocation de registres : si une variable nest pas vivante en entre dun


bloc on peut rassigner son registre.
- Elimination de code mort dans un bloc :
liminer les affectations aux variables qui ne sont pas vivantes en sortie de
bloc ni lues ultrieurement dans le bloc.

61

8.4.4

Propagation des copies

Pour pouvoir propager les les copies, on a besoin de connatre lensemble


In(B) de toutes les instructions du type x = y qui atteignent lentre dun
bloc B et lensemble Out(B) de toutes les instructions du type x = y qui
atteignent la sortie du bloc.
Une copie x = y est "produite" par B si elle appartient au bloc B et il ny a
pas daffectation ultrieure y dans B. Lensemble des copies produites par
B est P rod(B).
Une copie x = y est "supprime" par B si elle nappartient pas au bloc B
et x ou y sont dfinies dans B. Lensemble des copies supprimes par B est
Supp(B).
Les quations reliant ces ensembles sont les suivantes :
In(B1 ) = ,
pour B 6= B1 , In(B) = B 0 P red(B) Out(B 0 )
Out(B) = P rod(B) (In(B) Supp(B))
On effectue alors une analyse avant et une recherche de plus grand point
fixe.
Si le bloc B contient une utilisation de x, si la copie x = y est dans In(B) et
aucune affectation x ou y nest faite entre le dbut du bloc et lutilisation
de x, alors on peut remplacer x par y dans B.
Si on a pu remplacer toutes les utilisations de x par y, alors on peut supprimer la copie x = y.
Exemple :
L1 :
t1=a+b
x=y
if (t1 > 0) goto L2 else goto L3
L2 :
t2=b+1
y=2*t2
goto L5
L3 :
62

x=z
L4:
t2=x-1
L5:
t1=a+x
En rsolvant les quations, on trouve quaucune des deux copies natteint
B5 . Elles ne peuvent donc pas tre propages.

8.5
8.5.1

Optimisation des boucles


Dfinition des boucles et des dominants

Un ensemble B de blocs est une boucle de tte Bh ssi cet ensemble de


blocs vrifie :
pour tout bloc B de B, il existe un chemin dans le graphe de contrle
de flot, allant de Bh B, dont tous les sommets sont dans la boucle
et un chemin allant de B Bh , dont tous les sommets sont dans la
boucle,
si il existe un arc B B 0 du graphe de contrle de flot, avec B 0 dans
la boucle et B en dehors de la boucle, alors B 0 est gal Bh (Un seul
point dentre dans la boucle).
Un sommet B du graphe de contrle de flot domine le sommet B 0 ssi
tout chemin du graphe de contrle de flot, partant du bloc initial et arrivant
B 0 , passe ncessairement par B.
Par dfinition :
- tout bloc se domine lui-mme
- la tte de boucle domine les autres blocs de la boucle.
Lensemble des dominants du bloc B vrifie :
Dom(B) = {B} B 0 P red(B) Dom(B 0 )
Algorithme de calcul des dominants
Soient B1 , B2 , . . . Bn , les blocs du graphe de contrle de flot :
1. initialiser Dom(B1 ) {B1 } et pour tout i > 1, initialiser Dom(Bi )
lensemble des blocs,
2. pour tout i de 2 n, calculer :
Dom(Bi ) = {Bi } Bj P red(B) Dom(Bj )

63

10

3. tant que lun des ensemble Dom(Bi ) change, reprendre en (2).


Dans lexemple ci-dessus, les boucles sont :
{7, 8, 9, 10} et {3, 4, 5, 6, 7, 8, 9, 10}

8.5.2

Instructions invariantes dans une boucle

Ce sont les instructions qui calculent les mmes valeurs depuis lentre du
contrle dans la boucle jusqu la sortie de la boucle.
Une des optimisations importantes des boucles consiste dtecter les instructions invariantes, afin de les sortir de la boucle.
Algorithme de calcul des instructions invariantes
1. Marquer comme invariantes les instructions dont les oprandes sont
des constantes ou ont toutes leurs definitions visibles en dehors de la
boucle.
2. Rpter ltape 3 jusqu aucune nouvelle instruction invariante ne soit
marque.
3. Marquer comme invariantes les instructions dont les oprandes vrifient
une des conditions suivantes :
. tre une constante
. avoir toutes les dfinitions visibles lextrieur de la boucle
. avoir exactement une dfinition visible lintrieur de la boucle et
cette dfinition est marque.
64

Algorithme de dplacement de code hors de la boucle


Condition pour dplacer une instruction invariante (i) : x = y + z en dehors
de la boucle :
1. le bloc contenant (i) domine tous les blocs de sortie de boucle,
2. la boucle ne contient aucune autre dfinition de x,
3. aucune dfinition de x autre que (i) (cest--dire extrieure la boucle)
natteint une utilisation de x dans la boucle.
On peut alors dplacer linstruction (i) dans un bloc "preheader", cest--dire
un bloc plac immdiatement avant la boucle.

8.5.3

Remplacement des variables dinduction dans une boucle

Le but de cette optimisation est davoir moins de calculs effectus


chaque passage dans la boucle, ou alors des calculs moins coteux : rduction
de force.
On distingue deux types de variables dinduction :
variables dinduction de base = variables incrmentes ou dcrmentes dune quantit constante chaque passage dans la boucle :
i = i c, o c est un invariant de boucle,
variables dinduction drives = variables obtenues comme somme
ou produit (resp. soustraction ou division) dune autre variable dinduction et dun invariant de boucle :
k = a j ou k = j + b, o a et b sont des invariants de boucle et j une
variable dinduction,
condition que :
- la seule dfinition de j qui atteint k est dans la boucle,
- si j est elle-mme drive de i, il ny a pas de dfinition de i entre la
dfinition de j et celle de k.
Algorithme de remplacement des variables dinduction drives :
Soit k = a j (resp. k = j + b) une variable dinduction drive,
1. remplacer la dfinition de k par k = k 0 , o k 0 est une nouvelle variable,
2. si j est une variable dinduction de base, ajouter la fin du bloc o
j est initialise, une instruction de prtraitement k 0 = a j0 (resp.
k 0 = j0 + b) avec j0 la valeur initiale de j,
3. ajouter aprs toute instruction j = j c une instruction k 0 = k 0 a c
(resp. k 0 = k 0 c),
65

remarque : pour k 0 = k 0 a c : faire le calcul de la constante c0 = a c


dans un bloc de prtraitement, puis crire k 0 = k 0 c0 ,
4. remplacer toute utilisation de k par k 0 .
A la suite de cet algorithme, on peut ventuellement liminer du code mort
dans les cas suivant :
1. si j (dont k est drive) nest utilise dans la boucle que dans une
dfinition delle-mme et si j nappartient pas aux variables vivantes
des blocs de sortie de boucle, alors on peut supprimer j,
2. si j (dont k est drive) est utilise dans la boucle, dans une condition
de saut : if (j > N) goto L1 , remplacer ce saut par
if (k > a*N) goto L1 , si a positif, ou par
if (k < a*N) goto L1 , si a ngatif,
(resp. par if (k > Nb) goto L1 , si k = j b).
Exemple : On rappelle quune instruction x = a[k]; est traduite en
t0 = a
t1 = w * k
// w est la taille dun lment de pile
x = t0 + t1
Dans lexemple suivant w=4 .
L1 :
i=m-1
j=n
t0=a
t1=4*n
v=t0+t1
// quivaut v=a[n]
L2 :
i=i+1
t2=4*i
t3=t0+t2
// quivaut t3=a[i]
y=2*t2
if (t3 < v) goto L2 else goto L3
L3 :
j=j-1
t4=4*j
t5=t0+t4
// quivaut t5=a[j]
if (t5 > v) goto L3 else goto L4
66

L4:
if (j < i) goto L2
L5:
print(t3,t5)

else

goto L5

67

Chapitre 9

Allocation de registres
La valeur des variables doit tre stocke depuis la dfinition de la variable
jusqu ses diffrentes utilisations.
Deux possibilits : stocker la valeur en mmoire (sur la pile) ou stocker la
valeur dans un registre de la machine.
Problme : le nombre de registres est limit.
Il faut savoir sil est possible de stocker toutes les variables dans des registres,
car cest plus avantageux en temps daccs, ou si il est ncessaire de mettre
certaines variables sur la pile. Ces dcisions ont un fort impacte sur le temps
dexcution du programme.
On utilise les informations de lanalyse du flot de donnees :
. si une variable nest pas vivantes la fin dun bloc, alors on peut librer
son registre,
. si deux variables sont vivantes en mme temps, on ne peut pas les stocker
dans le me registres, il faudra deux registres diffrents.

9.1

Graphes dinterfrence des variables

Les sommets du graphes sont les variables du programme.


Il y a une arrte entre deux variables si elles sont simultanment vivantes en
sortie dune instruction, on dit quelles interfrent.
Pour savoir le nombre minimal de registres ncessaires pour excuter le programme, on utilise une heuristique de coloration de graphe.
Rappel : savoir si un graphe est coloriable en k couleurs avec k > 2, et de
telle sorte que deux voisins aient toujours des couleurs diffrentes, est NPcomplet.

68

On na donc pas dalgorithmes, juste des heuristiques.


Principe : si il existe un sommet x du graphe dinterfrence G tel que x a un
degr strictement plus petit que k, alors si G {x} est k-coloriable, G est
k-coloriable.

9.2

Heuristique de coloriage avec k couleurs

trouver un sommet de degr strictement plus petit que k,


enlever ce sommet du graphe,
rcursivement appliquer lheuristique de coloriage au reste du graphe,
remettre le sommet supprim dans le graphe,
le colorier avec la couleur disponible (il en existe ncessairement une).
Si lheuristique bloque alors on met en mmoire lune des variables qui a
caus le blocage et on recommence avec un sommet en moins dans le graphe.
Pour construire le graphe dinterfrence, ont relie par un arc particulier, appel arc "MOVE", les variables qui sont des copies lune de lautre. Ce type
darc nest pas pris en compte dans lheuristique de coloriage. On essaie cependant de trouver un coloriage qui assigne la mme couleur aux variables
lies par un arc "MOVE". Si cest le cas on peut alors fusionner ces deux
variables en une seule.
Exemple Soit le bloc suivant, dont les variables vivantes en sortie sont d, j, k.
g=M[j+12]
h=k-1
f=g*h
e=M[j+8]
m=M[j+16]
b=M[f]
c=e+8
d=c
k=m+4
j=b
Le graphe dinfrence de ce code est coloriable avec 4 couleurs.
Les variables d et c sont identifiables (remplacer tous les c par d).
Les variables j et b sont identifiables (remplacer tous les b par j).

69

9.3

Variables spilles

Si le nombre maximal de registres est fix et que le coloriage choue pour


une variable x, il faut stocker x en mmoire (on dit alors que x est une
variable "spille").
choisir une adresse mmoire mx pour stocker x,
pour chaque apparition de x dans le programme, introduire une nouvelle variable xj :
si linstruction utilise xj , rajouter juste avant M OV E(xj , mx ),
(remarque : pour cette instruction Def = {xj } et U se = )
si linstruction dfinit xj , rajouter juste aprs M OV E(mx , xj ),
(remarque : pour cette instruction Def = et U se = {xj })
en gnral, xj est vivante pendant deux ou trois instructions, donc
interfre peu avec les autres variables.
On peut tracer un nouveau graphe dinterfrence o le degr des nouvelles
varaibles xj est strictement infrieur au degr de x dans le prcdent graphe.
On peut donc le colorier avec un plus petit nombre de couleurs.
Exemple Dans lexemple prcdent, si la variable f est spille, le graphe
dinfrence est coloriable en 3 couleurs.

70

Chapitre 10

Le code compil
Definition : Lensemble des structures de donnes maintenues lxecution
(pile, tas, zone de mmoire statique) pour implmenter les concepts de haut
niveau est appel Environnement dexcution ("runtime environment").
Il est ncessaire de tenir compte du futur Environnement dexcution, lors
de la traduction en Code Intermdiaire :
comment les variables et les fonctions sont-elles stockes en mmoire ?
comment sont implments les appels de fonctions, les passages de
paramtres ?
comment sont implments les tableaux ?
comment est implmente lallocation et ventuellement la libration
de la mmoire ?

10.1

Organisation de lespace de travail

Si le langage source na pas simultanment des dfinitions de fonctions


imbriques et des fonctions comme valeur de retour dautres fonctions, alors
les appels de fonctions se comportent comme une pile :
variables locales cres lentre des fonctions
variables locales dtruites la sortie des fonctions.
Le bloc de mmoire du code compil est spar en quatre espaces :
1. le Code cible produit
2. la Zone de mmoire statique
3. la Pile des appels de fonctions
4. le Tas

71

Le bloc de la pile relatif un appel de fonction est appel bloc dactivation


("activation record" ou "stack frame" en anglais).
Les donnes dont la dure de vie est incluse dans le bloc dactivation dune
fonction (variables locales) seront alloues en pile ou en registres).
Les variables globales sont en zone de mmoire statique.
Toutes les autres informations (donnes alloues dynamiquement) sont contenues dans une zone spare, appele le tas.

10.2

Enregistrement dactivation

On gre la pile laide de deux pointeurs :


SP (Stack Pointer) est le pointeur de sommet de pile (la pile est souvent
impmente en ordre inverse : avancer le pointeur de pile correspond le
dcrmenter),
FP (Frame Pointer) est le pointeur vers le dbut des variables locales du bloc
dactivation courant.
Exemple : Lexpression BinOp Add E1 E2 correspond au calcul suivant
code(E1 ); code(E2 );
PILE[SP - 1] := PILE[SP - 1] + PILE[SP] ;
SP := SP - 1 :
Lorsquune fonction g appelle f (a1, ..., an), le bloc dactivation de f est
allou (dans la pile). Il contient :
1. les paramtres de lappel a1, ..., an (transmis par g),
2. ladresse de retour de f (adresse de la premire ligne de code excuter
aprs le retour de lenregistrement dactivation de f ),
3. le pointeur FP de g,
4. ltat machine (sauvegarde des registres de la machine) avant lappel
de f ,
5. les variables locales de f ,
6. les temporaires utiliss pendant les calculs de f .

72

Appel de fonction : appel de f par g :


1. g value les arguments a1, ..., an de f ,
2. g stocke dans le bloc dactivation de f son adresse de retour et son
pointeur F P ,
3. f sauvegarde ltat machine,
4. f initialise ses variables locales et excute son code.
Retour de fonction : retour de la fonction f :
1. f place la valeur de retour en suivant de lenregistrement dactivation
de g,
2. f restaure ltat machine,
3. f se branche ladresse de retour et efface de la pile son bloc dactivation,
4. g utilise la valeur de retour de f et restaure le pointeur de pile SP .

10.3

Allocation mmoire

Allocation statique :
on dispose dun pointeur GP (Global Pointer) sur la zone statique.
Une nouvelle adresse de variable globale est obtenue en dcalant le
pointeur de la zone statique.
La taille allouer doit tre connue la compilation (pas dallocation
dynamique dans cette zone).
Pas de fonctions rcursives utilisant cette zone (tous les appels utiliseraient la mme adresse).
Allocation en pile :
Pas de perte despace mmoire (les variables locales sont supprimes)
La taille de la mmoire librer (enregistrement dactivation au retour
dune fonction) est connue du compilateur.
Allocation dans le tas : allocation dynamique.
Le tas est souvent gr comme une suite de blocs mmoire libres ou
occups.
- Allocation explicite prvue par le programme source (ex : malloc).
Dans ce cas la libration de la mmoire doit aussi tre explicite.
- Allocation implicite lorsque le processeur le demande.
73

Problme du "rebut" : zone alloue dans le tas mais inaccessible. Les


langages forte allocation dynamique (Java, Lisp) pratiquent la rcupration du "rebut" par un ramasse-miettes (Garbage collector).
passage paramtres :
1. par valeur : crire directement les valeurs dans lenregistrement
dactivation de la fonction appele.
2. par rfrence (pointeur) : crire les adresses des paramtres dans
lenregistrement dactivation de la fonction appele.
3. par copie-restauration (valeur-rsultat) :
lappel, la fonction appelante copie les valeurs des paramtres
dans lenregistrement dactivation de la fonction appele,
au retour, la fonction appele copie les nouvelles valeurs des paramtres dans lenregistrement dactivation de la fonction appelante.
4. par nom : substitution textuelle des expressions passes en argument dans le corps de la fonction appele (implmente par le
passage de ladresse de lexpression).

10.3.1

En pratique

En java, une classe Frame.java implmentera les enregistrements dactivations. Une instance de cette classe sera associ chaque fonction.
Cette classe contiendra plusieurs variables statiques : les pointeurs
SP et F P (pointeurs sur la pile), le pointeur GB (pointeur sur le
tas), un pointeur RV (return value) vers ladresse de retour, un entier
W ordSize donnant la taille dun mot de pile.
Le back-end du compilateur rajoutera deux morceaux de code chaque
appel de fonction : un "prologue " rajout au dbut et un "pilogue"
rajout la fin.
Le prologue se charge daller chercher les paramtres de la fonction, l
o la machine cible les attend.
Lpilogue se charge de placer la valeur de retour, l o la machine
cible lattend.

74

Anda mungkin juga menyukai