Vous êtes sur la page 1sur 14

Chapitre I : Introduction à la compilation

I. Introduction
Dans les années 50 est apparu le besoin de langages de plus haut niveau que
l’assembleur, de façon à abstraire certaines particularités propres à la machine
(registres en nombre finis, instructions de branchement basiques, etc...) pour
mieux se concentrer sur les aspects algorithmiques et pouvoir passer à l’échelle
sur des projets plus complexes. Un des objectif était également de pouvoir écrire
un seul programme pour des architectures différentes. Pour pouvoir utiliser ces
langages, il faut alors soit disposer d’un interpréteur, c’est-à-dire un exécutable
qui va lire le programme et évaluer son contenu; soit traduire le programme en
code machine exécutable : on parle alors de compilation.
Ces langages se sont heurtés au scepticisme des programmeurs de l’époque
qui avaient l’habitude de produire des codes assembleurs très efficaces et qui
pensaient qu’il ne serait pas possible de produire automatiquement des
exécutables aussi efficaces à partir de langages de haut niveau. Un bon
compilateur ne peut donc se contenter de traduire naïvement le langage de haut
niveau, il doit l’optimiser. Ces optimisations, associées au gain d’échelle offert
par l’abstraction, a permis la généralisation de l’utilisation de langages de haut
niveau.
Un compilateur ne traduit pas forcément un langages en code machine, il
peut produire du code dans un langage intermédiaire qui sera ensuite lui-même
compilé (par exemple, le langage intermédiaire peut être du C) ou interprété (par
exemple, du bytecode).
Définition :
Un compilateur est un exécutable qui traduit un langage de haut niveau
vers un langage de plus bas niveau.
La qualification de haut ou bas niveau pour un langage est subjective.
Ainsi, C est un langage de haut niveau si on le compare à de l’assembleur, mais
les compilateurs pour certains langages produisent du C (l’avantage étant qu’il
existe ensuite des compilateurs de C vers de nombreuses architectures, ce qui
évite de devoir écrire un compilateur pour chacune d’elle). Par la suite, on se
contentera de parler de langage source et de langage cible.

1
Pr. Khalid El fahssi
Exemple :
Le premier compilateur optimisant, écrit en 1957, traduisait du Fortran en
code machine pour l’IBM 704. Les compilateurs de Fortran sont toujours parmi
les meilleurs à l’heure actuelle en terme d’optimisation. Ceci s’explique par la
relative simplicité du langage, mais aussi par l’utilisation de Fortran pour le
calcul scientifique qui a engendré le besoin d’obtenir du code très efficace.
Les compilateurs pour C produisent en général du code machine (exemple :
gcc). Le compilateur java d’oracle produit du bytecode qui est ensuite interprété
par une machine virtuelle, la JVM. Ocaml dispose de deux compilateur, ocamlc
et ocamlopt, produisant respectivement du bytecode et du code machine.
Un exécutable qui génère du PDF à partir d’un autre langage (exemple: LATEX,
SVG, PostScript, etc.) est aussi un compilateur.
Un préprocesseur peut également être vu comme un compilateur du
langage avec macros vers le langage pur. Lex et Yacc sont aussi des
compilateurs : ils traduisent des expressions régulières et des grammaires hors-
contexte vers du code C. (ie. l’acronyme de Yacc : Yet Another Compiler
Compiler).
En général, un compilateur ne se contente pas de traduire un langage dans
un autre, il est capable de signaler des erreurs de syntaxe, de sémantique (par
exemple via une vérification de type) si possible de façon compréhensible par
l’utilisateur, il fait des optimisations qui peuvent viser plusieurs objectifs parfois
contradictoires : vitesse d’exécution, taille du code, utilisation de la mémoire
(notamment pour les applications embarquées), etc…

II. Compilateurs vs Interpréteurs


Tous simplement, le compilateur traduit un programme écrit dans un langage
L1 appelé langage source (souvent un langage de haut niveau) en un programme
équivalent écrit en un langage L2 appelé langage cible (voir Figure 1). Un rôle
important du compilateur est de signaler les erreurs dans le programme source
qui sont détectées pendant le processus de traduction.

2
Pr. Khalid El fahssi
Dans le cas où le programme cible est un programme exécutable écrit en
langage machine, il peut ensuite être appelée par l'utilisateur pour traiter les
entrées et produire des résultats en sorties (voir Figure 2).

Un interpréteur est un autre type commun de traitement de langage. Au lieu de


produire un programme cible comme une compilation, un interpréteur semble
exécuter directement les opérations spécifiées dans le programme source sur les
entrées fournies par l'utilisateur (voir Figure 3).

a.Naissance des compilateurs


La vraie histoire des compilateurs a commencée en 1950, et en particulier
avec une machine appelée la 704 construite par IBM. Il était leur première
machine qui a connu un succès commercial, bien qu'il y ait eu une certaine

3
Pr. Khalid El fahssi
premières machines qu'ils avaient expérimentée. Mais, de toute façon la chose
intéressante à propos de la 704, et une fois les clients ont commencé à l’acheter
et l’utilisée, est qu'ils ont constaté que les coûts liés aux logiciels ont trop
dépassé le coût du matériel.
Ceci est important car le matériel de ces jours était extrêmement cher. La charge
du logiciel pour faire bon usage des ordinateurs devenait dominante, que le
matériel n’avait jamais coûté encore.
Ce qui a conduit un certain nombre de gens à réfléchir sur la façon dont ils
pourraient faire un meilleur travail de l'écriture de logiciels. Comment
pourraient-ils faire une programmation plus productive.
Les premiers efforts pour améliorer la productivité de la programmation ont été
appelé « speedcoding », développé en 1953 par John Backus. Speedcoding
permet l’amélioration de la vitesse du codage. C’est ce que nous appelons
aujourd’hui, un exemple précoce d'une interprète. Et comme tous les interprètes,
il y avait des avantages et des inconvénients. Le principal avantage est qu'il est
beaucoup plus rapide, pour développer les programmes. Donc en ce sens, le
programmeur était beaucoup plus productif, mais parmi ses inconvénients, les
programmes en speedcoding étaient dix à vingt fois plus lents. En plus,
l’interprète speedcoding avais besoins de 300 octets de mémoire.
Cela ne semble pas beaucoup, en fait, 300 octets, aujourd’hui, semblent comme
un très petit programme. Mais dans cette époque, vous devez garder à l’esprit,
que c'était 30% de la mémoire sur le Machine. Donc, ce fut 30% de la totalité de
la mémoire de la 704. Par conséquent speed coding ne peut devenir populaire.
Malgré tout, John Backus pensé qu'il était prometteur et il lui a donné l'idée
pour un autre projet.
Les applications les plus importantes de ces jours étaient les calculs
scientifiques, et les programmeurs ont pensé d’écrire des formules sous une
forme que la machine pourrait l’exécuter.
John Backus jugeait que le problème avec le speed coding est que les formules
étaient en fait interprétées. Alors, il pensait, s’il peut d'abord les traduire dans
une forme que la machine pourrait les exécuter directement pour que le code soit
plus rapide, tout en permettant aux programmeurs d'écrire des programmes à un
niveau élevé. C’est à ce moment que le projet FORTRAN était né.
Le projet FORTRAN a duré de 1954 à 1957. Ils ont pensé qu'il ne ferait que
prendre une année à construire le compilateur mais il finirait par prendre trois

4
Pr. Khalid El fahssi
ans. Ainsi, tout comme aujourd'hui, ils n'étaient pas très bons pour prédire les
projets de logiciels combien de temps prendrait. Mais c'
c'était
était un projet très réussi.
En 1958, plus de 50% de programmes étaient en FORTRAN. C'est une
adoption très rapide d'une nouvelle technologie. Nous serions heureux avec ce
genre du succès aujourd'hui, et bien sûr, à ce moment ils étaient ravis, et tout le
monde pensait que FORTRAN a relevé le niveau d'abstraction, l'amélioration de
la productivité des programmeurs, et a permis à chacun une meilleure utilisation
de ces machines.
À peu près au même moment, Noam Chomsky a commencé son étude de
la structure du langage naturel. Sa conclusion a finalement fait la construction de
compilateurs beaucoup plus facile. L'étude de Chomsky a conduit à la
classification des langues en fonction de leur complexité des règles précisant
leur structure et des algorithmes nécess
nécessaires
aires pour les reconnaître. Un des
niveaux de Chomsky, la grammaire hors hors-contexte,
contexte, s'est avéré le plus utile pour
les langages de programmation.
L'étude de l'analyse, la détermination d'algorithmes efficaces pour la
reconnaissance
econnaissance des langages hors-contexte, te, a été poursuivie dans les années
1960 et 1970 et a conduit à une solution assez complète de ce problème, qui est
la théorie sous-jacente
jacente de la plupart des cours de compilation.

b. Principe de Compilateur

Un compilateur est décomposé en deux phases pprincipales:


• Phases d’analyse: Permettent de dégager les constituants d’un
programme source dans une représentation intermédiaire

• Phases de synthèse: Permettent de construire le programme cible à partir


de cette représentation intermédiaire.

5
Pr. Khalid El fahssi
Exemple:
• Premier compilateur : compilateur Fortran de J. Backus (1957)
• Langage source : langage de haut niveau (C, C++, Java, Pascal, Fortran...)
• Langage cible : langage de bas niveau (assembleur, langage machine)

c. Structure de Compilateur

Figure 4 : Diff
Différentes phase de compilation

Partie frontale : indépendant de la machine

 Analyseur lexical
 Analyseur syntaxique
 Analyseur sémantique
 Générateur de code intermédiaire

Partie finale : dépendant de la machine

 Optimiseur de code
 Générateur de code final

Le processus de compilation est complexe. Il est pratiquement impossible de


le considérer comme constituant un seul objet indissociable. Les compilateurs
modernes contiennent deux (grandes) parties, dont chacune est souvent
subdivisée en plusieurs sous
sous-parties.
arties. Ces deux parties sont appelées partie
frontale (front-end)
end) et partie finale (back
(back-end).
end). Si nous examinons le processus
6
Pr. Khalid El fahssi
de compilation plus en détail, on voit qu'il fonctionne comme une séquence de
phases, dont chacune transforme une représentation du programme source à une
autre. Une décomposition typique d'un compilateur en phases est représentée sur
la Figure 4. Dans la pratique, plusieurs phases peuvent être regroupés, et les
représentations intermédiaires entre les phases groupées ne doit pas être
construit de manière explicite. La table de symboles, qui stocke des informations
sur le programme source entier, est utilisée par toutes les phases de la
compilation.

d. Les différentes phases de la compilation


i. Analyse
La phase d'analyse correspond à reconnaître qu'une entrée est un
programme corret du langage source. Cette analyse doit être la plus rapide
possible. C'est pourquoi on la structure en trois parties (analyse lexicale, analyse
syntaxique, analyse sémantique) chaque phase utilise des outils de plus en plus
complexes permettant d'avoir une compréhension de plus en plus ne du sens du
programme.

1. Analyse lexicale
Il s'agit de lire la suite de caractères un à un et de séparer les unités lexicales.
- Séparateurs, commentaires, mots clés.
- Identificateurs, entiers, flottants, symboles ou suites de symboles.
- Directives de compilation.
Ces entités correspondent à des expressions régulières. Certaines ambiguïtés
peuvent apparaître :
>= peut correspondre à une ou deux unités lexicales.
Le crible permet d'exécuter des actions au moment de la reconnaissance
de chaque unité lexicale. Cette phase permet de traiter et de classifier les unités.
On pourra par exemple séparer les mots clés des identificateurs.
Il y a a priori une infinité d'unités lexicales reconnaissables, pourtant la
grammaire ne peut traiter qu'un alphabet fini. Heureusement la grammaire n'a
besoin de distinguer que des lasses d'unités lexicales (identificateurs, valeurs
numériques, commentaires, . . .). On distingue donc pour haque unité lexicale, sa
classe (aussi appelée token) et sa valeur qui sera utilisée dans la construction de
l'arbre de syntaxe abstraite.
À la fin de l'analyse lexicale, on a une suite de tokens associés à des
valeurs. Des programmes tels que lex, flex permettent d'engendrer
automatiquement des analyseurs lexicaux à partir d'une description des classes
d'unités lexicales sous forme d'expressions régulières.

7
Pr. Khalid El fahssi
A titre d'exemple, l'analyse lexicale de l’énoncé position = initiale +
vitesse * 60 devrait donner la suite de jetons suivante:

• <position, identificateur>
• <=, symb-affectation>
• <initiale, identificateur>
• <+, symb-plus>
• <vitesse, identificateur>
• <*, symb-mult>
• <60, constante-entière>

Les identificateurs, tels que les noms des variables et des procédures, ainsi
que leurs attributs, sont stockés dans une table appelée table des symboles voir
Figure 5.

La sortie de l’analyse lexicale pour l’instruction position = initiale +


vitesse * 60 est la suite de jetons suivante: id(1) aff id(2) add id(3) mul nbr(60)
Quand l’analyse lexicale détecte une entité lexicale invalide, il rapporte une
erreur. Les erreurs sont telles que : identificateurs trop longs ou illégaux,
caractères ou nombres illégaux, etc.

2. Analyse syntaxique
Il s'agit de reconnaître la structure du programme. L'analyse syntaxique
détecte que le programme donné en entrée satisfait les règles grammaticales du
langage utilisé.
L'analyse syntaxique peut se faire suivant des méthodes ascendantes ou
descendantes.

8
Pr. Khalid El fahssi
Le langage devra être descriptible par une grammaire ayant de bonnes
propriétés pour obtenir une analyse efficace. Des programmes tels que
Yacc/Bison permettent d'engendrer automatiquement des analyseurs syntaxiques
à partir d'une description de la grammaire du langage lorsque elle-ci a de bonnes
propriétés.
Les actions associées aux règles de grammaire permettent de construire un arbre
de syntaxe abstraite comme résultat de l'analyse syntaxique.
Par exemple la forme d’un arbre syntaxique concrète pour notre énoncé
position = initiale + vitesse * 60 est présenté dans la Figure 6.

3. Analyse sémantique
Il s'agit d'analyser l'arbre de syntaxe abstraite pour vérifier des propriétés
dites statiques telles que la bonne utilisation des variables, le typage corret des
expressions.
Si les opérateurs arithmétiques sont surchargés comme les opérations
arithmétiques qui peuvent s'appliquer aux entiers et aux flottants, la phase
d'analyse sémantique permettra de résoudre cette ambiguïté et d'appeler les
opérations machines adaptées.
On peut encore utiliser des techniques d'analyse de flots de données ou
d'interprétation abstraite pour détecter des risques d'erreurs à l'exécution ou
effectuer des optimisations indépendantes du langage cible.
Par exemple détecter qu'une variable n'a pas été initialisée avant son utilisation,
propager une valeur de constante connue, transformer des programmes récursifs
terminaux en programme itératifs, détecter des morceaux de codes inatteignables
9
Pr. Khalid El fahssi
(code-mort). Chaque analyse coûte cher mais peut permettre d'optimiser le code
engendré ou bien de détecter les erreurs. Le choix des analyses dépend de ce qui
est possible (beaucoup des propriétés que l'on souhaiterait établir comme le non-
dépassement des bornes dans un tableau ou la terminaison des programmes ne
sont pas décidables) et de e qui est faisable, nombreuses propriétés décidables
correspondent à des algorithmes coûteux.
par exemple, les types (entier, réel, pointeur sur un tableau d'entiers) des objets
impliqués. Cela permet la vérification des erreurs sémantiques et l'insertion
d'une conversion de type si nécessaire. Par exemple, on ne peut pas additionner
un réel avec une chaîne de caractères, ou affecter une variable à un nombre. Par
exemple, si la vitesse est un nombre réel, nous avons besoin d'insérer des
opérateurs de conversion "inttoreal" comme indiqué dans la Figure 7.

ii. Synthèse

La synthèse correspond à la phase de reconstruction de l'expression du


langage cible à partir de l'arbre syntaxique. Dans le cas de la compilation d'un
langage de programmation on distinguera les phases suivantes:

1. Allocation mémoire
L'allocation mémoire détermine comment ordonner les entités du
programme dans les mots mémoires de la machine. Elle doit tenir compte de la
taille de es mots mémoires et des contraintes d'alignement de la machine. Par
exemple un nombre ne pourra être chargé, écrit ou utilisé dans une opération que
s'il est situé sur une frontière du mot.
Le compilateur peut être amené à réserver de l'espace en mémoire pour
des calculs intermédiaires ou le passage des arguments d'une procédure. Il devra
rendre et espace mémoire lorsqu'il n'est plus utile.
10
Pr. Khalid El fahssi
L'utilisateur peut souhaiter manipuler des données de taille variable, la
gestion de cette espace mémoire dont l'utilisation n'est pas connue a priori peut
être effectuée par l'utilisateur (on parle d'allocation statique ) ou bien par le
compilateur (on parle d'allocation dynamique ). Celui-ci doit alors mettre en
oeuvre des programmes de « rammasse-miettes » (GC pour Garbage Colletor ou
Glaneur de Cellules) chargés de récupérer l'espace mémoire réutilisable.

2. Génération de code
Elle construit les instructions du programme cible. Il faut déterminer les
séquences d'instructions cibles correspondant aux expressions de l'arbre
syntaxique. Il faut allouer judicieusement les registres de la machine qui ont un
accès plus rapide que la mémoire.
Donc, à partir de l'arbre syntaxique et plus précisément, d'une entité syntaxique,
un code intermédiaire simple sera produit. Plusieurs formes pour le code
intermédiaire peuvent être conçues. Les plus utilisés sont:
‒ Le code post-fixe: C'est un code dans lequel, tout opérateur apparait après ses
opérandes. C'est un code préconisé car les opérateurs et les opérandes dans ce
code apparaissent dans le même ordre qu'à l’exécution.
‒ le code 3 trois adresses: Il est caractérisé par le fait qu'une instruction du code
contient un seul opérateur. Et de cette manière, il se rapproche de l'assembleur.
La différence majeure existant entre ce code et le code assembleur est que
contrairement au code intermédiaire, l'assembleur spécifie des registres dans ses
instructions.

Le code de la représentation Intermédiaire de notre instruction


position = initiale + vitesse * 60 aura par exemple la forme le suivante :

3. Optimisations de code cible


Il s'agit de détecter des séquences de code cible qui peuvent être optimisées. On
peut chercher à diminuer la taille du code engendré dans le cas de processeurs
aux ressources limitées.
11
Pr. Khalid El fahssi
Par exemple Le code de la représentation intermédiaire (R.I) de notre instruction
position = initiale + vitesse * 60 va être optimisé comme suit :
Code R.I -------> Code R.I optimisé
1 = inttofloat(60) t1 = id3 * 60.0
t2 = id3 * t1 id1 = id2 + t1
t3 = id2 + t2
id1 = t3

La partie finale synthétise le programme cible de la représentation intermédiaire


produite par la partie frontale. Cette partie est généralement indépendante du
langage source.
4. Génération du code machine.

La génération de code objet est la phase finale de la compilation. Elle


nécessite la connaissance de la machine cible (réelle, virtuelle ou abstraite). Il
faut connaître le jeu d'instruction de la machine, les différents modes
d’adressage et notamment de ses possibilités en matière de registres, piles, etc.
Cette phase génère du code objet relogeable, i.e. relatif à l’origine 0. Elle traduit
chaque instruction du code intermédiaire en langage machine cible. Par exemple
la R.I de notre instruction de calcule de position sera transformé en code
machine suivant :

Un problème qui se pose lors de la génération du code objet est la gestion


efficace de l'attribution des registres aux variables des instructions. A tout
moment, il faut mémoriser les registres affectés à des variables et ceux qui ne le
sont pas.
Un autre problème que l'on rencontre lors de cette phase est la redondance des
instructions générées.

iii. Phases parallèles


1. Gestion de la table des symboles
La table des symboles est la structure de données utilisée servant pour
stocker les informations qui concernent les identificateurs du programme source

12
Pr. Khalid El fahssi
(par exemple leur type, leur emplacement mémoire, leur portée, visibilité,
nombre et type et mode de passage des paramètres d'une fonction,...). Le
remplissage de cette table a lieu lors des phases d'analyse. Les informations
contenues dans la table sont nécessaires lors des analyses syntaxique et
sémantique, ainsi que lors de la génération de code.
1- L'analyse lexicale insère les identificateurs, les constantes ainsi que les labels
dans cette table.
2- L'analyse syntaxique, elle, associe à chaque identificateur son type, à chaque
fonction, son nombre d'arguments ainsi que ses paramètres formels etc.
3- Le générateur du code intermédiaire lui, avant de générer une instruction telle
que x=y*z par exemple, doit accéder à la table pour repérer les types des
opérandes afin de vérifier la compatibilité des types. Si le langage permet le
mixage des types, une instruction spécifiant une conversion de type sera générée
et le générateur doit mémoriser le nouveau type dans la table.
4- Lors de l'optimisation des sous-expressions communes, à tout identificateur
résultant d'une expression à éliminer doit être substitué à l'identificateur
résultant de l'expression à garder.
Le résultat de cette modification doit figurer dans la table afin de poursuivre le
processus.

2. Gestion des erreurs

Chaque phase peut rencontrer des erreurs. Cependant, après avoir détecté
une erreur, une phase doit la traiter de telle façon que la compilation puisse
continuer et que d'autres erreurs dans le programme source puissent être
détectées. Un compilateur qui s'arrête à la première erreur n'est pas non plus très
performant. Bien sûr, il y a des limites à ne pas dépasser et certaines erreurs (ou
un trop grand nombre d'erreurs) peuvent entraîner l'arrêt de l'exécution du
compilateur.

Exemple : Phases d’un compilateur pour l’instruction y :=a*b+30

13
Pr. Khalid El fahssi
Phases de compilation de l’instruction y :=a*b+30

14
Pr. Khalid El fahssi

Vous aimerez peut-être aussi