Uploaded by talebbechir123

cours1

advertisement
Compilation
Cours de Master 1ère année
Ph. Clauss
Références bibliographiques
●
Compilateurs : Principes, techniques et outils
A. Aho, R. Sethi, J. Ullman
●
Advanced Compiler Design and Implementation.
Steven S. Muchnick
●
Lex & Yacc
J.R. Levine, T. Mason, D. Brown
●
Modern Compiler Design
D. Grune, H.E. Bal, C.J.H. Jacobs, K.G. Langendoen
2
I : Introduction
3
Compilation
L'objectif ?
Hommes et machines n'utilisent pas le même langage
Langage de programmation
Langage machine
haut niveau :
supporte une syntaxe riche et
expressive, modularité,
structuration
structuratio
bas niveau :
reflète l'architecture matérielle au
niveau binaire, opérations explicites
sur les unités fonctionnelles et
mémoires
⇒ Plus généralement,
compilation = outils de traduction et de
transformation d'un langage source vers
un langage cible
4
Différents niveaux de langage
langage machine :
une suite binaire directement interprétable par les microprogrammes de la
machine. Les opérations et opérandes de chaque instruction dans le jeu
d'instructions de la machine sont spécifiées par un code machine (formé de 0 et 1).
langage d'assemblage :
proche du langage machine : mais les instructions sont représentées par des
symboles mnémoniques (beaucoup plus faciles à retenir que le code machine).
Ex : ADD R1,R2
Son exécution nécessite une traduction : le traducteur est appelé assembleur.
assembleur
langage de commande (script) :
permet de communiquer avec le système d'exploitation. Son rôle essentiel est de
permettre de lancer l'exécution de codes exécutables (en langage machine).
5
Différents niveaux de langage
langage de haut-niveau :
adapté à l'expression des algorithmes et structures de données, indépendant du
jeu d'instructions de la machine. Son exécution nécessite une traduction dont la
difficulté est proportionnelle au niveau du langage.
langage intermédiaire :
langage interne à un compilateur pour la représentation intermédiaire d'un
programme, commune à tous les langages sources pris en compte par le
compilateur. Le compilateur effectue des analyses et des transformations
(optimisations) sur cette représentation intermédiaire, avant de générer le code
binaire final.
Exemples : - compilateur GCC, 3 langages intermédiaires
(RTL, GENERIC, GIMPLE)
- compilateur CLANG-LLVM, langage LLVM-IR (bitcode)
- bytecode Java, .NET
langage de macro-commandes (pre-processing) :
langage d'instructions au compilateur (inclusion de fichiers, substitution de formes
6
mnémoniques de suites d'instructions en langage source, ...)
Les différents outils de traduction
On peut distinguer 3 types d'outils :
- Interpréteur :
traduit et exécute un programme écrit dans un langage source en code
machine « à la volée »
- Machine virtuelle ou compilateur dynamique
(JIT, « Just-In-Time » compiler) :
prend en entrée un programme en langage intermédiaire (bytecode ou
bitcode), le traduit, l'optimise et l'exécute à la volée.
- Compilateur (statique) :
prend en entrée un programme en langage source, le traduit, l'optimise, et
génère un programme écrit en langage objet
(un code écrit en langage objet ou code objet contient du code écrit
en langage machine ainsi que d'autres informations nécessaires à
l'édition de liens et au déboggage).
7
Les cousins du compilateur
Avec l'utilisation des techniques de génie logiciel (modularité, généricité), la
création d'un code exécutable peut requérir plusieurs autres outils)
- Préprocesseur :
traduction des macro-commandes, suppression des commentaires, inclusion de
fichiers séparés, ...
- Éditeur de liens :
Regroupement de plusieurs code objets (éventuellement issus de différents
compilateurs) en un code exécutable.
8
Différents niveaux de compréhension d'un texte
(ou d'un programme)
Initialement un texte (ou un programme) = une suite de caractères
1er niveau : reconnaître les mots du texte, déterminer si chaque
groupe de caractères appartient à un lexique
 transformer la suite de caractères en une suite de mots
= analyse lexicale
2ème niveau : exhiber la structure du texte, déterminer s'il est
conforme à une syntaxe ou une grammaire
 transformer la suite de mots en une structure hiérarchique
= analyse syntaxique
3ème niveau : comprendre le sens du texte
= analyse sémantique
9
Structure d'un compilateur
= Plusieurs sous-tâches interconnectées (phases)
Une phase prend en entrée une représentation du texte source et
produit en sortie une autre représentation du même texte source.
Une décomposition typique d'un compilateur comporte 6 phases
10
Structure d'un compilateur
11
Table des symboles
La table des symboles est une structure de données contenant un
enregistrement pour chaque identificateur, muni de champs
permettant de stocker des informations associées. Cette structure
permet d'accéder rapidement à ces informations.
Sa gestion consiste à enregistrer les identificateurs utilisés dans le
programme source et à collecter les informations associées à
chaque identificateur (type, portée (région du programme dans
laquelle il est valide), ...).
12
Traitement des erreurs
Chaque phase peut rencontrer des erreurs. Cependant, une phase doit
les traiter de façon à ce que que la compilation puisse continuer et que
d'autres erreurs puissent être détectées.
Un compilateur qui s'arrête à la première erreur n'est pas très utilisable.
13
Analyse lexicale
Analyse lexicale (scanning)
- transforme un flot de caractères en un flot d'unités lexicales (tokens)
dont chacun représente une suite de caractères.
Une unité lexicale (token) peut être : un mot clé, un identificateur, une
constante, un opérateur ou un séparateur.
Exemple : position := initiale + vitesse * 60
devient : ID AFF ID PLUS ID MULT CST
14
Analyse lexicale
Certaines unités lexicales comme les constantes ou les identificateurs
ont une “valeur lexicale” qui est la suite des caractères qu'elles
représentent (lexème).
Exemple : (ID,”position”) AFF (ID,”initiale”) PLUS (ID,”vitesse”) MULT
(CST,”60”)
Lorsqu'un identificateur est rencontré, il est entré dans la table des
symboles (s'il n'y est pas déjà).
15
Analyse syntaxique
Analyse syntaxique (parsing)
- vérifie que la suite de tokens est une construction permise
(conforme à la grammaire définissant le langage source)
Exemple : CST AFF PLUS MULT ID ID ID est une construction
incorrecte
16
Analyse syntaxique
- rend explicite la structure hiérarchique
Exemple : ID AFF (ID PLUS (ID MULT CST))
- produit un arbre de syntaxe abstraite
(syntaxe abstraite ≠ syntaxe concrète)
17
Analyse sémantique
Analyse sémantique
- vérifie la cohérence des types (et éventuellement décore ou enrichit
l'arbre par exemple par des opérations de conversion de type)
18
Génération de code intermédiaire
Génération de code intermédiaire
- construit une représentation intermédiaire du programme source = un
code pour une machine abstraite (dont les instructions sont plus
évoluées qu'une machine standard) .
Ce code peut prendre plusieurs formes. Parmi les formes possibles :
code à 3 adresses (séquences d'instructions a au plus 3 opérandes)
Exemple :
temp1 := EntierVersReel(60)
temp2 := id3 * temp1
temp3 := id2 + temp2
id1 := temp3
19
Optimisation de code
Optimisation de code
tente d'améliorer l'efficacité du code intermédiaire.
Exemple :
temp1 := id3 * 60.0
id1 := id2 + temp1
id4:= id3
temp2:= id4 * 60.0
id5:= id5 + temp2
temp1:= id3 * 60.0
id1:= id2 + temp1
id5:= id5 + temp1
20
Génération de code
Génération de code
- produit du code objet :
. choisit les emplacements mémoires pour les données
. sélectionne le code machine pour implémenter les instructions du
code intermédiaire
. alloue les registres (un aspect important de la performance)
Exemple : en utilisant les registres R1 et R2
MOVF id3, R2
MULF #60.0, R2
MOVF id2, R1
ADDF R2, R1
MOVF R1, id1
21
Compilation
Regroupement de phases
- en pratique, il arrive souvent que l'on regroupe plusieurs phases :
1) Parties frontale (frontend) et finale (backend)
Les différentes phases sont souvent réunies en une partie frontale
(indépendante de la machine) et finale (dépendante de la machine).
22
Compilation
2) Passes
On implémente habituellement plusieurs phases en une seule passe (une
lecture de la représentation en entrée + une écriture de la représentation en
sortie). Il est préférable d'avoir peu de passes pour réduire le temps de
compilation.
Par exemple:
L'analyseur syntaxique peut faire appel à l'analyseur lexical chaque fois qu'il a
besoin d'une nouvelle unité lexicale.
L'analyseur syntaxique peut produire directement du code intermédiaire (on
parle de traduction dirigée par la syntaxe)
syntaxe
Ainsi, on peut souvent regrouper les quatre premières phases en une passe.
L’optimisation comprend souvent de nombreuses passes, qui se répètent
23
Compilation
Objectif de ce cours
Comprendre/Maîtriser la construction d'un compilateur.
NB: Cette construction s'appuie sur des résultats de la théorie des
langages :
* Expression rationnelle et automate
(pour l'analyse lexicale)
* Grammaire algébrique et automate à pile
(pour l'analyse syntaxique)
* Grammaire attribuée
(pour la génération de code intermédiaire)
24
Compilation
NB: Il existe des outils pour la construction de compilateurs :
* LEX (flex)
(pour l'analyseur lexical)
* YACC (bison)
(pour l'analyseur syntaxique)
* LLVM (http://www.llvm.org)
(pour une infrastructure complète de compilateur)
25
II : Analyse lexicale
26
Rôle d'un analyseur lexical
Lexèmes (tokens)
code source
analyseur
lexical
analyseur
syntaxique
...
table des
symboles
●
●
●
●
Séparer l'analyse lexicale de l'analyse syntaxique
Réduire la complexité du compilateur et la complexité de conception de
ces deux modules
Augmenter la flexibilité du compilateur : portabilité, maintenabilité
Augmenter l'efficacité du compilateur
Implémentation
Utiliser un générateur d'analyseurs lexicaux (Flex)
●
Écrire l'analyseur lexical dans un langage évolué
●
Mots et langages formels
Un mot sur un alphabet A est une suite finie de symboles pris dans
A
A peut être l'ensemble des caractères, dans ce cas les lexèmes sont
des mots
alphabet
mots
code ASCII
{ 0, 1 }
caractères
analyse lexicale
caractères
lexèmes
analyse syntaxique
lexèmes
programmes
Le mot vide est noté ε, sa longueur est 0
Un langage formel est un ensemble de mots
Expressions rationnelles
On forme des expressions à partir des symboles (éléments de l'alphabet) et de ε en
utilisant
- l'union notée | (ou U)
- la concaténation (pas de symbole)
- l'itération notée *
On utilise souvent des opérations supplémentaires (abréviations) :
+
pour l'itération au moins une fois
? pour l'option zéro ou une fois
. pour tout l'alphabet
[...] pour une union de symboles
Priorités entre opérateurs : itération, concaténation, union
Expressions rationnelles
Identificateurs en C
lettre = A | B | ... | Z | a | b | ... | z
chiffre = 0 | 1 | ... | 9
id = (lettre | _) (lettre | _ | chiffre)*
Nombres sans signe en C
chiffre = 0 | 1 | ... | 9
chiffres = chiffre chiffre*
frac = . chiffres
exp = (E | e) (+ | - | ε) chiffres | ε
num = (chiffres (frac | . | ε) | frac) exp
Les mêmes avec les abréviations
chiffre = 0 | 1 | ... | 9
chiffres = chiffre+
frac = . chiffres
exp = ((E | e) (+ | -) ? chiffres)?
num = (chiffres (frac | .)? | frac) exp
Lexèmes ou tokens
Définition des espaces blancs
delim = \b | \t | \n
ws = delim*
On dit habituellement lexème pour "catégorie de lexèmes" :
expression
lexème
attribut
expression
lexème
attribut
<
relop
lt
ws
-
-
<=
relop
le
if
if
-
==
relop
eq
else
else
-
!=
relop
ne
id
id
pointeur
>
relop
gt
num
num
valeur
>=
relop
ge
etc.
Automates finis
Les analyseurs lexicaux utilisent des graphes appelés automates finis
- les sommets sont appelés états
- les transitions sont orientées et étiquetées
- certains états sont finaux et comportent une action
lettre | _ | chiffre
11
lettre
_
12
autre
Identificateurs en C
13
return(ID)
Automates finis
0
autre
1
<
2
return(relop, lt)
3
return(relop, le)
=
>
4
autre
=
=
7
=
5
return(relop, gt)
6
8
return(relop, ge)
return(relop, eq)
!
9
=
10
Opérateurs de comparaison
return(relop, ne)
Automates finis
chiffre
1
4
15
E|e
chiffre
.
+|-
16
.
20
chiffre
chiffre
chiffre
17
18
autre
chiffre
21
chiffre
E|e
chiffre
2
2
23
autre
chiffre
24
.
.
25
chiffre
Nombres sans signe en C
chiffre
26
autre
19
Implémentation en C
Un programme C qui reconnaît dans le début d'une chaîne de caractères un des
lexèmes définis par les automates
/* Recherche de l'état initial du prochain automate */
int state = 0, start = 0 ;
int lexical_value ;
int fail() {
forward = token_debut ;
switch(start) {
case 0 : start = 11 ; break ;
case 11 : start = 14 ; break ;
case 14 : start = 22 ; break ;
case 22 : recover() ; break ;
default : /* erreur */ }
return start ; }
token nexttoken() {
while (1) {
switch(state) {
case 0 : c = nextchar() ;
if (c==blank||c==tab||c==newline) {
state = 0 ; debut ++ ; }
else if (c == '<') state = 1 ;
else if (c == '>') state = 4 ;
else if (c == '=') state = 7 ;
else if (c == '!') state = 9 ;
else state = fail() ;
break ;
/* ... cas 1 - 10 ... */
case 11 : c = nextchar() ;
if (isletter(c)||c=='_') state = 12 ;
else state = fail() ;
break ;
case 12 : c = nextchar() ;
if (isletter(c)||c=='_') state = 12 ;
else if (isdigit(c)) state = 12 ;
else state = 13 ;
break ;
case 13 : retract(1) ; install_id() ;
return(gettoken()) ;
/* ... cas 14-26 ... */
}
}
}
Automates déterministes
Un automate fini est déterministe si :
- il possède au plus un état initial
- de chaque état part au plus une flèche étiquetée par un symbole
donné
Exemple d'automate non déterministe :
a|b
a
1
2
a
Exemple d'automate déterministe :
b
1
a
a
b
2
Automates déterministes
Un automate déterministe représentant un ensemble de mots peut être
utilisé pour reconnaître l'un des mots dans une chaîne de caractères : il
suffit de partir de l'état initial, de suivre les flèches et de voir si l'état dans
lequel on arrive est final.
Tout automate fini est équivalent à un automate fini déterministe : il existe
un automate fini déterministe qui reconnaît le même langage.
Utilisation de Flex
spécification
Flex
flex
lex.yy.c
compilateur C
caractères
(code source)
a.out
4 étapes :
- créer sous éditeur une spécification Flex (expressions
rationnelles)
- traiter cette spécification par la commande flex
- compiler le programme source C obtenu
- exécuter le programme exécutable obtenu
lexèmes
...
Spécifications Flex
Un programme Flex est fait de trois parties :
déclarations
%%
règles de traduction
%%
fonctions auxiliaires en C
Les règles de traduction sont de la forme
p1
{ action1 }
p2
{ action2 }
...
pn
{ actionn }
où chaque pi est une expression rationnelle et chaque
action une suite d'instructions en C.
Exemple
%{
/* Partie en langage C : définitions de constantes,
déclarations de variables globales, commentaires... */
%}
delim
[ \t\n]
letter
[a-zA-Z]
%%
{delim}* { /* pas d'action */ }
if
{ return IF ; }
then
{ return THEN ; }
else
{ return ELSE ; }
{letter}({letter}|[0-9])*
{ yyval = install_id() ;
return ID ; }
([0-9]+(\.[0-9]*)?|\.[0-9]+)((E|e)(\+|-)?[0-9]+)?{
yyval = install_num() ; return NUMBER ;
}
Exemple
"<"
{ yyval = LT ; return RELOP ; }
"<="
{ yyval = LE ; return RELOP ; }
%%
install_id() {
/* fonction installant dans la table des symboles le
lexème vers lequel pointe yytext et dont la longueur
est yylength. Renvoie un pointeur sur l'entrée dans
la table */
}
install_num() {
/* fonction calculant la valeur du lexème (→ attribut)
*/
}
Spécifications Flex
Les commentaires /* ... */ ne peuvent être insérés que dans une
portion en C :
- dans la partie déclaration, seulement entre %{ et %} ;
- dans la partie règles, seulement dans les actions ;
- dans la partie fonctions auxiliaires, n'importe où.
Dans les règles
pi
{ actioni }
les expressions rationnelles pi ne peuvent pas contenir d'espaces
blancs (ou alors dé-spécialisés).
Dans la partie règles, si une règle commence par un espace blanc,
elle est interprétée comme du langage C et est insérée dans
lex.yy.c au début de la fonction qui renvoie le prochain lexème
(utilisable pour déclarer des variables locales).
Segmentation du code source
par l'analyseur lexical (1/3)
L'analyseur lexical produit par Flex
- commence à chercher les lexèmes au début du code source ;
- après chaque lexème reconnu, recommence à chercher les
lexèmes juste après
Exemple : si piraté est reconnu, raté n'est pas reconnu
Si aucun lexème n'est reconnu à partir d'un point du code
source
l'analyseur affiche le premier caractère sur la sortie standard et
recommence à chercher à partir du caractère suivant
Segmentation du code source
par l'analyseur lexical (2/3)
Si plusieurs lexèmes sont reconnus à partir d'un point
du code source (conflit)
1. Deux lexèmes de longueurs différentes
C'est le plus long qui gagne
Exemple : [a-zA-Z_] [a-zA-Z_0-9]*
Segmentation du code source
par l'analyseur lexical (3/3)
2. Deux lexèmes de même longueur
Ils commencent au même point et terminent au même point :
s'il y a conflit, c'est qu'ils sont issus de deux règles
différentes. C'est la règle qui apparaît la première dans la
spécification Flex qui gagne
Exemple : conflit entre [a-zA-Z_] [a-zA-Z_0-9]* et while
Mettre l'exception avant la règle
En dehors de ce cas, l'ordre des règles ne joue pas
III : Analyse syntaxique
49
Analyse syntaxique
Introduction
Rôle de l'analyse syntaxique :
- déterminer si une suite de tokens est conforme à la grammaire
- déterminer les erreurs de nature syntaxique et le cas échéant les
réparer
En pratique, d'autres tâches sont réalisées simultanément à
l'analyse syntaxique :
- ajouter des informations à la table des symboles
- effectuer des vérifications de type
- produire du code intermédiaire
50
Analyse syntaxique
Méthodes d'analyse syntaxique
La plupart des méthodes d'analyse tombent dans 2 classes :
- méthodes descendantes (top-down parsing)
- méthodes ascendantes (bottom-up parsing)
51
Analyse syntaxique
Dans la suite nous illustrons les différences entre ces 2 méthodes
par l'exemple du langage des expressions arithmétiques
Le langage est décrit par la grammaire algébrique suivante :
G0 : E → E + T | T
T→T * F | F
F → ( E ) | id
Par convention, les lettres majuscules (ici E, T et F) sont des
symboles non-terminaux et les autres symboles (ici * + ( ) et id)
sont des symboles terminaux (identifient des tokens).
Nous considérons l'analyse de la chaîne : (id+id)*id
52
Analyse syntaxique
* Pour les analyses descendantes,
- l'arbre de syntaxe est construit de la racine jusqu'aux feuilles.
- l'analyse s'appuie sur des dérivations gauche de la suite de tokens :
on remplace toujours en premier le non-terminal le plus à gauche.
53
Analyse syntaxique
G0 : E → E + T | T
T→T * F | F
F → ( E ) | id
(id+id)*id
Dérivation gauche de cette chaîne :
E g T g T * F g F * F
g (E) * F g (E + T) * F
g (T + T) * F g (F + T) * F
g (id + T) * F g (id + F) * F
g (id + id) * F g (id + id) * id
NB : A chaque étape, on sélectionne une règle
qui permet de dériver le reste de la chaîne
(Ce qui peut a priori nécessiter des retours-arrière.
On peut éviter les retours-arrière en utilisant
l'information des premiers symboles qui dérivent
d'une règle)
54
Analyse syntaxique
* Inversement pour les analyses ascendantes,
- l'arbre de syntaxe est construit en remontant à la racine depuis les
feuilles.
- l'analyse s'appuie sur des dérivations droite de la suite de tokens : on
remplace toujours en premier le non-terminal le plus à droite.
55
Analyse syntaxique
G0 : E → E + T | T
T→T * F | F
F → ( E ) | id
(id+id)*id
Dérivation droite de la chaîne :
E d T d T * Fd T * id
d F * id d (E) * id d (E + T) * id
d (E + F) * id d (E + id) * id
d (T + id) * id d (F + id) * id
d (id + id) * id
NB: A chaque étape, on sélectionne une partie
au début de la chaîne qui dérive d'un non-terminal
et on la remplace par ce non-terminal. L'inverse
de la suite de règles utilisées est une dérivation
droite de la chaîne
56
Related documents
Download