Chapitre 5 : Génération de code
Nous allons compléter l'analyseur sémantique construit au
chapitre 3 pour générer le P-Code
correspondant au programme PP1 analysé.
Une première chose à faire est de décider de l'allocation des
variables. Ensuite, chaque reconnaissance d'une règle de la grammaire
déclenche la génération de P-Code à l'aide des procédures
GENERER1
(génération
d'une instruction P-Code sans opérande) et
GENERER2
(génération
d'une instruction P-Code à un opérande) dont l'écriture est
détaillée à la fin de ce chapitre.
5.1 Allocation mémoire
Au niveau du langage PP1 (comme dans les autres langages de haut
niveau), on ne se préoccupe pas de la gestion de la mémoire ; on
manipule celle-ci par l'intermédiaire de symboles.
C'est le rôle du compilateur d'allouer ces symboles en
mémoire. à chaque symbole, le compilateur doit associer un
emplacement mémoire dont la taille dépend du type du symbole.
Une manière simple et naturelle de faire est de choisir les adresses
au fur et à mesure de l'analyse des déclarations en incrémentant
un offset qui indique la place occupée par les déclarations
précédentes (variable OFFSET
).
à la fin des déclarations, il est possible de déterminer
l'emplacement mémoire à réserver dans la pile au début de
l'exécution du programme (instruction P-Code INT
).
Pour chaque symbole, son adresse d'allocation est stockée dans la
table des symboles :
var
TABLESYM : array [TABLEINDEX] of record
NOM : ALFA ;
CLASSE : CLASSES ;
ADRESSE : integer
end ;
OFFSET : integer ;
On modifie la procédure ENTRERSYM
pour tenir compte de cette allocation
mémoire :
procedure ENTRERSYM(C:CLASSES) ;
begin
if DERNIERSYM = INDEXMAX then ERREUR ;
DERNIERSYM := DERNIERSYM + 1 ;
with TABLESYM [DERNIERSYM] do begin
NOM := SYM ;
CLASSE := C ;
if C = VARIABLE then begin
ADRESSE := OFFSET ;
OFFSET := OFFSET + 1
end
end
end ;
Le champ ADRESSE
n'est utilisé que pour les variables ;
néanmoins, on l'utilisera pour stocker la valeur des constantes ; on
modifie la procédure CONSTS
en conséquence (à faire en exercice).
5.2 Génération de début et fin de programme
Une fois l'allocation des données réalisée, il est nécessaire
de réserver l'emplacement suffisant dans la pile P-Code. Cette
réservation est faite lors de l'analyse d'un BLOCK
par la
génération d'une instruction P-Code INST
:
procedure BLOCK ;
begin
OFFSET := 0 ;
if TOKEN = CONST_TOKEN then CONSTS ;
if TOKEN = VAR_TOKEN then VARS ;
GENERER2 (INT, OFFSET) ;
INSTS
end ;
Lors de la terminaison de l'analyse d'un programme, il est
nécessaire de générer une instruction P-Code d'arrêt du
programme, HLT
:
procedure PROGRAM ;
begin
TESTE (PROGRAM_TOKEN) ;
TESTE_ET_ENTRE (ID_TOKEN, PROGRAMME) ;
TEST (PT_VIRG_TOKEN) ;
BLOCK ;
GENERER1 (HLT) ;
if TOKEN <> POINT_TOKEN then ERREUR
end ;
5.3 Génération des expressions
Dans un analyseur récursif descendant comme celui que nous
construisons, lorsque l'on termine une procédure d'analyse c'est que
l'on a déjà analysé correctement les phrases correspondant aux
procédures appelées dans cette procédure.
Par exemple lors de l'analyse de l'expression a + b
, EXPR
va
appeler TERM
deux fois, une fois pour analyser a
et une fois
pour analyser b
; l'analyse du +
se fait au niveau de EXPR
.
Si les deux appels réussissent (c'est le cas dans notre exemple) et
que le +
est bien reconnu, EXPR
réussit.
Lorsque la génération est dirigée par la syntaxe, on adopte le
même raisonnement : lorsqu'une procédure se termine, on
considère que la génération des phrases analysées par les
procédures appelées est terminée. Il ne reste plus qu'à
générer le code pour l'addition, c'est-à-dire simplement
l'instruction P-Code ADD
.
5.3.1 Génération des facteurs (FACT)
On commence donc par le génération des facteurs pour lesquels on
laisse sur la pile P-Code une valeur :
procedure FACT ;
begin
if TOKEN = ID_TOKEN then begin
TESTE_ET_CHERCHE (ID_TOKEN, [CONSTANTE, VARIABLE]);
with TABLESYM [PLACESYM] do
case CLASSE of
CONSTANTE : GENERER2 (LDI, ADRESSE) ;
VARIABLE : begin
GENERER2 (LDA, ADRESSE) ;
GENERER1 (LDV)
end ;
PROGRAMME : ;
end
end
end ;
else if TOKEN = NUM_TOKEN then begin
GENERER2 (LDI, VAL) ;
NEXT_TOKEN
end
else begin
TESTE (PAR_OUV_TOKEN) ;
EXPR ;
TESTE (PAR_FER_TOKEN)
end
end ;
5.3.2 Génération des termes (TERM)
De même, un terme laisse sur la pile P-Code la valeur du terme. Il
est nécessaire de mémoriser le token correspondant à
l'opération avant l'analyse de l'opérande gauche du terme
(variable OP
) :
procedure TERM ;
var OP : TOKENS ;
begin
FACT ;
while TOKEN in [MULT_TOKEN, DIV_TOKEN] do
begin
OP := TOKEN ; (* memorise l'operation *)
NEXT_TOKEN ;
FACT ;
if OP = MUL_TOKEN
then GENERER1 (MUL)
else GENERER1 (DIV)
end
end ;
5.3.3 Génération des expressions (EXPR)
L'analyse d'une expression laisse aussi une valeur sur la pile P-Code
; le code est similaire à celui de l'analyse des termes :
procedure EXPR ;
var OP : TOKENS ;
begin
TERM ;
while TOKEN in [PLUS_TOKEN, MOINS_TOKEN] do
begin
OP := TOKEN ; (* memorise l'operation *)
NEXT_TOKEN ;
TERM ;
if OP = PLUS_TOKEN
then GENERER1 (ADD)
else GENERER1 (SUB)
end
end ;
5.4 Génération des conditions
La génération des conditions (procédure
COND
) est calquée sur celle des
expressions. à faire en exercice.
5.5 Génération des instructions simples
Il n'y pas de code à générer lors de l'analyse du bloc
d'instructions (INSTS
) ou d'une instruction (INST
). On
détaille ici la génération à réaliser lors de l'analyse
d'une instruction d'affectation et des instructions d'entrée/sortie.
5.5.1 Génération d'une affectation
Une affectation A := expression
est générée suivant le modèle
|
LDA <adresse de A> |
|
empile l'adresse de A |
|
|
<code> |
|
empile la valeur de l'expression |
|
|
STO |
|
stocke la valeur de l'expression dans A |
|
Le P-Code <code>
dépose la valeur de expression
sur la pile ;
il est généré lors de l'analyse de expression
par l'appel EXPR
.
On a donc :
procedure AFFEC ;
begin
TESTE_ET_CHERCHE (ID_TOKEN, VARIABLE) ;
GENERER2 (LDA, TABLESYM [PLACESYM]. ADRESSE) ;
TESTE (AFFEC_TOKEN) ;
EXPR ;
GENERER1 (STO)
end ;
5.5.2 Génération des instructions d'entrée/sortie
Le code à générer pour une instruction d'écriture telle
write (e1, e2, e3)
est le suivant :
|
<code1> |
|
empile la valeur de l'expression e1 |
|
|
PRN |
|
imprime cette valeur |
|
|
<code2> |
|
empile la valeur de l'expression e2 |
|
|
PRN |
|
imprime cette valeur |
|
|
<code3> |
|
empile la valeur de l'expression e3 |
|
|
PRN |
|
imprime cette valeur |
|
Les P-Codes <code1>
, <code2>
et <code3>
sont générés
lors de l'analyse des expressions e1
, e2
et e3
par des appels
à EXPR
. On modifiera la procédure
ECRIRE
en conséquence.
Le code à générer pour une instruction de lecture telle
read (v1, v2, v3)
est le suivant :
|
LDA <adresse de v1> |
|
empile l'adresse de la variable v1 |
|
|
INN |
|
lit un entier, le stocke dans v1 |
|
|
LDA <adresse de v2> |
|
empile l'adresse de la variable v2 |
|
|
INN |
|
lit un entier, le stocke dans v2 |
|
|
LDA <adresse de v3> |
|
empile l'adresse de la variable v3 |
|
|
INN |
|
lit un entier, le stocke dans v3 |
|
Modifier la procédure LIRE
en
conséquence.
5.6 Génération des instructions avec rupture de séquence
Nous étudions ici la génération de code pour les instructions de
rupture de séquence (if COND then INST
et while COND do INST
).
Cette génération est faite sur les modèles suivants :
|
|
|
code généré pour COND |
|
|
|
|
if not COND then goto LABEL |
|
|
|
|
code généré pour INST |
|
|
LABEL |
|
suite... |
|
pour l'instruction if
et
|
DEBUT |
|
code généré pour COND |
|
|
|
|
if not COND then goto LABEL |
|
|
|
|
code généré pour INST |
|
|
|
|
goto DEBUT |
|
|
LABEL |
|
suite... |
|
pour l'instruction while
.
Le problème est que lors de la génération du saut conditionnel
à LABEL
, on ne connaît pas encore la valeur de ce label
puisqu'on ne connaît pas a priori la longueur du code
généré pour
INST
.
On va donc générer des instructions de saut incomplètes (sans
numéro d'instruction) et mémoriser les numéros des instructions
incomplètes pour pouvoir les compléter lorsque l'information sera
disponible1.
Cette facilité de compléter une instruction préalablement
générée est offerte par l'intermédiaire des deux procédures
NEXT_INST
et REMPLIR_INST
:
procedure NEXT_INST (var COMPT:integer) ;
procedure REMPLIR_INST (NUM_INST, DEP:integer) ;
la procédure NEXT_INST
retourne dans le compteur indiquant le
numéro de la prochaine instruction qui sera générée ; la
procédure REMPLIR_INST
complète l'instruction de saut
NUM_INST
avec le numéro d'instruction DEP
.
On modifie donc la procédure SI
en conséquence :
procedure SI ;
var SAUT, SUITE : integer ;
begin
TESTE (IF_TOKEN) ;
COND ;
TESTE (THEN_TOKEN) ;
NEXT_INST (SAUT) ;
GENERER2 (BZE, 0) ; (* 0 car incomplet *)
INST ;
NEXT_INST (SUITE) ;
REMPLIR_INST (SAUT, SUITE)
end ;
On fait de même pour la procédure TANTQUE
:
procedure TANTQUE ;
var DEBUT, SAUT, SUITE ; integer ;
begin
TESTE (WHILE_TOKEN) ;
NEXT_INST (DEBUT) ;
COND ;
TESTE (DO_TOKEN) ;
NEXT_INST (SAUT) ;
GENERER2 (BZE, 0) ; (* 0 car incomplet *)
INST ;
GENERER2 (BRN, DEBUT) ;
NEXT_INST (SUITE) ;
REMPLIR_INST (SAUT, SUITE)
end ;
5.7 Procédures de génération de P-Code
On reprend les déclarations de la section 1.6 :
|
type |
|
MNEMONIQUES = (ADD,SUB,MUL,DIV,EQL,NEQ,GTR,LSS,GEQ,LEQ, |
|
|
|
|
PRN,INN,INT,LDI,LDA,LDV,STO,BRN,BZE,HLT) ; |
|
|
|
|
INSTRUCTION = record |
|
|
|
|
MNE : MNEMONIQUES ; |
|
|
|
|
SUITE : integer |
|
|
|
|
end |
|
|
var |
|
PCODE : array [0 .. TAILLECODE] of INSTRUCTION ; |
|
|
|
|
PC : integer ; |
|
Les fonctions de génération de code GENERER1
et GENERER2
s'écrivent simplement :
procedure GENERER1 (M:MNEMONIQUES) ;
begin
if PC = TAILLECODE then ERREUR ;
PC := PC + 1 ;
with PCODE [PC] do
MNE := M
end ;
procedure GENERER2 (M:MNEMONIQUES ; A:integer) ;
begin
if PC = TAILLECODE then ERREUR ;
PC := PC + 1 ;
with PCODE [PC] do begin
MNE := M ;
SUITE := A
end
end ;
Les procédures NEXT_INST
et REMPLIR_INST
s'écrivent :
procedure NEXT_INST (var COMPT:integer ) ;
begin
COMPT := PC + 1
end ;
procedure REMPLIR_INST (NUM_INST, DEP : integer) ;
begin
PCODE [NUM_INST]. SUITE := DEP
end ;
5.8 Travaux dirigés et travaux pratiques
-
Implanter l'allocation des variables en mémoire et stockage des
valeurs des constantes dans la table des symboles : modification des
procédures
ENTRERSYM
et CONSTS
(section 5.1). On testera la réalisation
par l'extension de l'affichage de la table des symboles
(exercice 4
).
-
Ajouter la possibilité de déclarer des constantes avec une valeur
signée et des constantes définies par des constantes
précédemment déclarées, comme par exemple
const
MAX = +456 ;
MIN = -456 ;
PETIT = -MAX ;
- Implanter les procédures de génération de P-Code
GENERER1
et
GENERER2
ainsi que les procédures utilitaires de générations
de P-Code associées (section 5.7). Sous
quelle forme générer le code ? Si le code généré doit être
rechargé par le chargeur de P-Code, il paraît plus
intéressant de passer par une forme non lisible (par un humain !),
mais plus compacte du code. Proposer un tel codage.
En complément, et pour faciliter le test du compilateur ou la mise au
point de programmes PP1, il est aussi intéressant de pouvoir obtenir une
forme lisible du P-Code généré. Quelles solutions sont envisageables
pour ce faire ? En implanter une.
- Lorsqu'une erreur est détectée, il est nécessaire de poursuivre
la compilation. Il est cependant normal de ne pas continuer la
génération de code. Pourquoi ? Mettre en oeuvre cette
interruption de la génération.
- Implanter la génération de code pour les expressions
EXPR
et
les instructions simples (affectation, lecture écriture). Tester
cette implantation.
- On implantera maintenant les conditions
COND
et les structures de
contrôles présentées : if
et while
.
- On complétera les structures de contrôle
if
et while
par des
constructions repeat ... until
et if ... then ... else
. En
définir la syntaxe, la sémantique et l'implantation.
- Définir et implanter une structure à choix multiples telle le
case
Pascal. Que faire lorsqu'aucune des << branches >> du case ne
convient ? Envisager la possibilité d'une alternative otherwise
qui
est exécutée dans ce cas.
- Comment introduire une boucle
for
à la Pascal ? En Pascal, la
variable de contrôle de la boucle ne peut pas être modifiée à
l'intérieur de la boucle. Comment imposer cette contrainte ?
Pourquoi, à votre avis, la valeur de l'indice des boucles for
n'est pas définie à la sortie de la boucle en Pascal ?
Certains pensent que la variable de contrôle de la boucle doit
être une vraie variable locale et donc être implicitement
déclarée au début de la boucle. Comment prendre cela en compte ?
- Comment intégrer une instruction
goto
en PP1 ?
- 1
-
La technique de génération de code détaillée ici est
directement liée au langage cible utilisé, le P-Code défini au
chapitre 1. Dans le cas de génération
d'un langage cible dans lequel les labels d'instruction sont définis
par le programmeur et non implicites (nod'instruction), la
technique de génération de code est différente. L'instruction de
saut peut être générée complètement avec un label défini
au sein du compilateur ; ce label est mémorisé et sera utilisé
pour l'écriture explicite du label de l'instruction sur laquelle
est effectuée le branchement.