Précédent Index Suivant

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

  1. 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 ).

  2. 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 ; 
    
  3. 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.

  4. 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.

  5. Implanter la génération de code pour les expressions EXPR et les instructions simples (affectation, lecture écriture). Tester cette implantation.

  6. On implantera maintenant les conditions COND et les structures de contrôles présentées : if et while.

  7. 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.

  8. 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.

  9. 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 ?

  10. 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.

Précédent Index Suivant