Topic outline

  • Compilation

    Compilation

    Réalisé par:

    Dr. ZAIDI Sofiane

    Objectifs:

    Objectifs

    • Comprendre comment les programmes sont compilés puis exécutés.
    • (Re)découvrir l’intérêt de l’étude de la compilation.
    • (Re)comprendre le fonctionnement et la construction des compilateurs.
    • (Re)prendre connaissance des outils fondamentaux pour le travail sur fichiers formatés
    • (Re)visiter les aspects ouverts au niveau recherche.

    Connaissances préalables recommandées : Théorie des langages

    Public cible:


    Public

    Ce  cours  est  orienté  vers  les  étudiants  de  troisième année Licence, spécialité  (Systèmes d'Information),  Filière  ( Informatique)  et  aussi  à  toute  personne  souhaitant d' acquérir des informations sur La compilation.

    Plan global du contenu

    Plan

    Ce cours est structuré en dix principaux chapitres et ce conformément au programme officiel (harmonisation de 2016-2017) :

                                                                              Chapitre 01 : Introduction 

                                                                              Chapitre 02 : Compilation

                                                                              Chapitre 03 : Analyse lexicale

                                                                              Chapitre 04 : Analyse Syntaxique

                                                                              Chapitre 05 : Analyse descendante

                                                                              Chapitre 06 : Analyse ascendante

                                                                              Chapitre 07 : Traduction dirigée par la syntaxe

                                                                              Chapitre 08 : Contrôle de type

                                                                              Chapitre 09 : Environnement d’exécution

                                                                              Chapitre 10 : Génération de code

    Mode  d’évaluation :

    evaluation

    Contrôle  (TD+TP)  50%  +  Examen  final  50%

    Auteur :

    Auteur

    Dr. ZAIDI Sofiane
    E-mail: zaidi.sofiane@univ-oeb.dz
    Maitre  de conférence classe  A,  Faculté  des  sciences  exactes  et  des  sciences  de  la  nature  et  de  la  vie, Département  InformatiqueUniversité  d'Oum  El  Bouaghi.

    Références :

    references

    1. Alfred Aho, Ravi Sethi et Jeffrey Ullman « Compilers, Principles techniques and tools » Addison-Wesley, 1986

    2. Aho A., Sethi R., Ullman J., "Compilateurs : Principes, techniques et outils", Inter-éditions, 1991 et Dunod, 2000

    3. Drias H., "Compilation: Cours et exercices", OPU, 1993

    4. Wilhem R., Maurer D., "Les compilateurs: Théorie, construction, génération", Masson, 1994

    5. Alfred Aho, Monica Lam, Ravi Sethi et Jeffrey Ullman Compilateurs principes, techniques et outils, 2ème édition. Pearson Education, 2007 

    6. Andrew Appel Modern compiler implementation in JAVA, 2nd edition, Cambridge University Press, 2002. 

    7. John Hopcroft, Rajeev Motwani, Jeffrey Ullman Introduction to Automata Theory, Languages and Computation, 2ème édition Pearson Education International, 2001. 

    8. Michael Sipser Introduction to the Theory of Computation PWS Publishing Company, 1997.

  • Chapitre 1 : Introduction

    chap1

  • Chapitre 04 : Analyse Syntaxique


    chap4


    L'analyseur syntaxique obtient une chaîne d'unités lexicales de l'analyseur lexical, comme illustre à la figure 4.1, et vérifie que la chaîne peut être engendrée par la grammaire du langage source.

    syn

    Figure 4.1: Emplacement d'un analyseur syntaxique dans un modèle de compilateur

    Les méthodes utilisées (par l'analyse syntaxique) sont soit descendantes soit ascendantes. Comme leurs noms l'indique, les analyseurs descendants construisent des arbres d'analyse de haut en bas, alors que les analyseurs ascendants partent des feuilles et remontent vers la racine.

    Dans les deux cas, l'entrée de l'analyseur est parcourue de la gauche vers la droite, un symbole à la fois.

    Les méthodes descendantes ou ascendantes les plus efficaces travaillent uniquement sur des sous classes de grammaires hors contexte, mais certaines de ces sous classes sont suffisamment expressives pour décrire la plupart des constructions syntaxiques des langages de programmation.

    4.1 Dérivation la plus à gauche et arbre de dérivation

    4.2 Grammaire ambiguë

    Une grammaire ambiguë peut avoir plus d'un arbre syntaxique qui engendre une chaîne donnée d'unités lexicales. Comme une telle chaîne a habituellement plus d'une signification, on a besoin de travailler avec des grammaires non ambiguës.

    Exemple

    chaîne → chaîne + chaîne| chaîne - chaîne|0|1|2|3|4|5|6|7|8|9

    La figure 4.2 montre qu'une expression comme 9-5+2 a plus d'un arbre syntaxique. Les deux arbres pour 9-5+2 correspondent aux deux manières de parenthéser l'expression (9-5)+2 et 9-(5+2). Le deuxième parenthésage donne à l'expression la valeur 2 plutôt que la valeur habituelle 6.

    ambigue

    Figure 4.2: Deux arbres syntaxiques pour 9-5+2

    4.3 Grammaire et langages de programmation

    4.4 Analyseurs syntaxiques et leurs types

    Il y deux grands types d’analyseurs syntaxiques :

    • Les méthodes descendantes

    – Rapides : temps dans O(n).

    – Simples.

    – Ne s’accommodent que de certaines grammaires : celles d’une des classes LL(k).

    – Variante LL(1) étudiée dans ce module.

    • Les méthodes ascendantes

    – Rapides : temps dans O(n).

    – Plus complexes que les méthodes descendantes.

    – Ne s’accommodent que de certaines grammaires : celles d’une des classes LR(k).

    – Les classes LR(k) sont plus grandes que les classes LL(k).

    4.5. Outils en pratique

  • Chapitre 05 : Analyse descendante

    chap5

    L'analyse descendante peut être considérée comme une tentative pour déterminer une dérivation gauche associée à une chaîne d'entrée.

    Elle peut-être aussi vue comme une tentative pour construire un arbre d'analyse de la chaîne d'entrée, en partant de la racine et en créant les nœuds de l'arbre suivant un ordre prédéfini.

    L'analyse descendante peut impliquer des retours arrière (nécessité de passages répétés sur la chaîne d'entrée).

    Exemple

    Considérons la grammaire

    S → cAd

    A → ab | a

    et la chaîne d'entrée w = cad.

    On commence par construire initialement un arbre qui contient un seul nœud étiqueté S. Un pointeur d'entrée repère c (le premier symbole de w), nous utilisons la première production de S pour développer l'arbre et obtenir

    syn1

    La feuille la plus à gauche, étiquetée c, correspond au premier symbole de w. Nous avançons maintenant le pointeur d'entrée sur a (second symbole de w), et nous considérons la feuille suivante étiquetée A. Nous pouvons alors développer A en utilisant la première alternative pour A et nous obtenons l'arbre suivant :

    syn2

    Nous avons alors une concordance avec le second symbole d'entrée ; nous avançons donc le pointeur à d (troisième symbole en entrée), et comparons d avec la feuille suivante étiquetée b.

    Comme b et d sont différents, nous signalons un échec et retournons à A pour voir s'il n'existe pas une autre alternative de A, non encore essayé et qui serait susceptible de produire une concordance.

    En retournant à A, nous devons remettre le pointeur d'entrée en position 2, la position qu'il avait quand nous sommes arrivés sur A la première fois.

    Nous essayons maintenant la seconde alternative de A et obtenons l'arbre de la figure suivante :

    syn3

    La feuille a correspond au second symbole de w et la feuille d au troisième symbole. Comme nous avons produit un arbre d'analyse pour w, nous nous arrêtons et annonçons le succès final de l'analyse.

    Remarque

    Une grammaire récursive à gauche peut faire boucler un analyseur par descente récursive même s'il possède un mécanisme de retour arrière.

    En effet dans le cas de A → Aα|ab|a, en essayant de développer A, on peut éventuellement se retrouver en train de développer A de nouveau, et cela sans avoir consommé de symbole en entrée. ⇒ Nous devons éliminer la récursivité à gauche.

    (Notons que le terminal d'entrée ne change que quand un terminal de la partie droite est accepté).

    Suppression de la récursivité gauche

    Définition

    Une grammaire est récursive à gauche si elle contient un non-terminal A tel qu'il existe une dérivation , où α est une chaîne quelconque.

    Considérons tout d'abord le non terminal A dans les deux productions :

    A → Aα| β où α et β sont des suites de terminaux et de non terminaux qui ne commencent pas par A

    Par exemple             expr → expr + terme|terme         A = expr, α=+terme et β =terme.

    Une application répétée de cette production fabrique une suite de α à droite de A comme la figure 5.1. Quand A est finalement remplacé par β, on obtient un β et une suite éventuellement vide de α.

    syn4

    On peut obtenir le même effet, en réécrivant la production définissant A de la manière suivante:

    A → βR

    R → αR|ε

    Ici R est un nouveau terminal. La production R→ αR est récursive à droite.

    syn5

    Exemple

    expr → expr + terme |terme

    devient :

    expr → terme R

    R → + terme R | ε

    Quelque soit le nombre de A-productions, il est possible d'éliminer les récursivités à gauche immédiates par la technique suivante :

    Dans un premier temps, on groupe les A-productions comme suit:

    A → Aα1| Aα2| …| Aαm|β1|β2|….|βn

    Où aucune βi ne commence par un A. Ensuite on remplace les A-productions par:

    A → β1A'|β2A'|….| βnA'

    A' → α1A'| α2A'| …| αm A'|ε (on suppose que αi ≠ ε)


    5.1 Analyse LL(1) (principe)
    5.2 Table d'analyse
    5.3 Grammaire LL(1)

  • Chapitre 6 : Analyse ascendante

    chap6

    Dans ce chapitre, nous introduisons un modèle général d'analyse syntaxique ascendante, connu sous le nom d'analyse par décalage-réduction. Une méthode beaucoup plus générale, appelée analyse LR, est présentée à la section qui suit. Elle est utilisée dans un grand nombre de constructeurs automatiques d'analyseurs syntaxiques.

    L'analyse par décalage-réduction a pour but de construire un arbre d'analyse pour une chaîne source en commençant par les feuilles (le bas) et en remontant vers la racine (le haut). Ce processus peut être considéré comme la "réduction" d'une chaîne w vers l'axiome de la grammaire. A chaque étape de réduction, une sous-chaîne particulière correspondant à la partie droite d'une production est remplacée par le symbole de la partie gauche de cette production. Si la sous chaîne est choisie correctement à chaque étape, une dérivation droite est ainsi élaborée en sens inverse.

    Exemple

    Soit la grammaire :

    S → aABe

    A→ Abc|b              

    B →d

    La phrase abbcde peut être réduite vers S par les étapes suivantes :

    abbcde

    aAbcde                                  production utilisée A→ b

    aAde                                      production utilisée A→ Abc

    aABe                                     production utilisée B→ d

    S                                            production utilisée S → aABe

    On parcourt abbcde à la recherche d'une sous-chaîne qui corresponde à la partie droite d'une production. Les sous-chaînes b et d conviennent. Choisissons la sous chaîne b la plus à gauche (ce n’est pas une règle) et remplaçons la par A.

    Maintenant les sous-chaînes Abc, b et d correspondent aux parties droites de production. On va choisir la production A → Abc malgré que b est la sous chaîne la plus à gauche qui corresponde à une partie droite de production (on va voir ultérieurement les règles de choix entre les chaînes). Nous obtenons aAde.

    Remplaçons d par d par B (B→d) nous obtenons aABe.

    Nous pouvons maintenant remplacer cette chaîne tout toute entière par S. Nous avons donc par une séquence de quatre réductions été capables de réduire abbcde vers S. Ces réductions, élaborent en sens inverse la dérivation droite suivante :

    asc

    Implantation à l'aide d'une pile de l'analyse par décalage-réduction

    Une bonne façon est d'utiliser une pile pour conserver les symboles grammaticaux et un Tampon d'entrée pour contenir la chaîne ω à analyser.

    Nous utilisons le symbole $ pour marquer à la fois le fond de pile et l'extrémité droite du tampon d'entrée.

    Initialement la pile est vide et la chaîne ω est dans le tampon d'entrée.

    PILE             ENTREE

    $                       ω$

    L'analyseur opère en décalant zéro, un ou plusieurs symboles d'entrée du tampon vers la pile jusqu'à ce qu'un manche β se trouve en sommet de pile.

    L'analyseur réduit alors β vers la partie gauche de la production appropriée.

    L'analyseur recommence ce cycle jusqu'à ce qu'il détecte une erreur ou jusqu'à ce que la pile contienne l'axiome et que le tampon d'entrée soit vide

    PILE         ENTREE

    $S                     $

    S'il atteint cette configuration, l'analyseur s'arrête et annonce la réussite finale de l'analyse.

    Exemple

    La table suivante montre les actions que doit effectuer un analyseur par décalage-réduction sur la chaîne id1+id2*id3 sur la grammaire : E→ E+E |E*E| (E) | id en utilisant la première dérivation:

    asc2

    asc3

    6.1 Analyse LR (principe)

    6.2 Analyse LR(0)

    6.3 Analyse SLR(1)

    6.4 Analyse LR(1) Analyse LALR(1)

  • Chapitre 07 : Traduction dirigée par la syntaxe

    chap7

    Une Définition Dirigée par la Syntaxe (DDS) est un formalisme permettant d’associer des actions à une règle de production de la grammaire. On appelle ainsi DDS la donnée d’une grammaire et des règles sémantiques. On parle de grammaire attribuée.

    Dans une DDS, on a les éléments suivants :

    • Chaque symbole de la grammaire (VT ou VN) possède un ensemble d’attributs (variables) pouvant être synthétisés ou hérités de ce symbole. Si un nœud d’arbre syntaxique (étiqueté par un symbole) est représenté par un enregistrement, un attribut correspond à un nom de champ de cet enregistrement.

    • Chaque règle de production de la grammaire possède un ensemble de règles sémantiques qui permettent de calculer la valeur des attributs associés au symbole apparaissant dans la production

    • Une règle sémantique est une suite d’instructions algorithmiques (elle peut contenir des affectations, des instructions d’affichage, des instruction de contrôle, etc.)

    Les règles sémantiques impliquent des dépendances entre les attributs. Ces dernières sont représentées par un graphe de dépendance (GD) à partir duquel en déduit un ordre d’évaluation des règles sémantiques.

    Remarque

    Un arbre syntaxique muni des valeurs d’attributs à chaque nœud est appelé arbre décoré (ou annoté). Le processus de calcul des valeurs des attributs aux nœuds est appelé décoration (ou annotation) de l’arbre.

    7.1 Grammaires attribuées

    7.1.1 Attributs synthétisés

    Un attribut synthétisé d'un nœud est calculé à partir des attributs au niveau des fils de ce nœud dans l’arbre syntaxique. Les attributs synthétisés sont très utilisés en pratique. Une DDS qui utilise uniquement les attributs synthétisés est appelée Définition S-attribuée.

    Un arbre syntaxique pour une définition S-attribuée est toujours décoré en évaluant les règles sémantiques pour les attributs de chaque nœud du bas vers le haut (peut être adapté à une analyse ascendante : grammaire LR, par exemple).

    Exemple

    7.1.2 Attributs hérités

    Un attribut hérité est un attribut dont la valeur, à un nœud d’un arbre syntaxique, est définie en fonction du père et/ou des frères de ce nœud. Il est souvent plus naturel d’utiliser des attributs hérités qui sont bien adaptés à l’expression des dépendances contextuelles.

    Exemple

    7.1.3 Structure d’une DDS

    Dans une DDS, chaque production de la grammaire (A → a) possède un ensemble de règles sémantiques de la forme :

    b := f(C1, C2, …, Ck)

    f : est une fonction, souvent écrite sous forme d’expressions et parfois sous forme d’un appel de procédure.

    b : est soit un attribut synthétisé de A, soit un attribut hérité d’un symbole en partie droite de la règle.

    C1, C2, …, Ck : sont des attributs de symboles quelconques dans la règle de production.

    7.2 Schéma de traduction

    Un schéma de traduction est comme une grammaire attribuée mais précise quand on fait les actions pendant un parcours de l'arbre en profondeur, elles sont insérées dans les membres droits des règles.

    • Si une action calcule un attribut d'un non-terminal du membre droit, elle doit être placée avant lui
    • Si une action utilise un attribut d'un non-terminal du membre droit, elle doit être placée après lui

    Exemple : traduction postfixe des expressions additives

      E --> T R

      R --> addop T { print(addop.lexeme) } R | e

      T --> num { print(num.val) }

    7.2.1. Traduction descendante

    Pour l'analyse descendante, on élimine la récursivité à gauche dans la grammaire. Il faut aussi adapter les attributs

    Exemple

      E --> E + T  E.val := E1.val + T.val

      E --> E - T  E.val := E1.val - T.val

      E --> T  E.val := T.val

      T --> ( E )   T.val := E.val

      T --> chiffre  E.val := chiffre.val

    Élimination de la récursivité à gauche

      E --> T { E'.he := T.val } E' {E.val := E'.sy }

      E' --> + T { E'1.he := E'.he + T.val } E' {E'.sy := E'1.sy }

      E' --> -  T { E'1.he := E'.he - T.val } E' {E'.sy := E'1.sy }

      E' --> e {E'.sy := E'.he }

      T --> ( E ) {T.val := E.val }

      T --> N {T.val := N.val }

    L'attribut E'.he sert à transmettre la valeur de l'expression située à gauche

    schema

    7.2.2 Traduction descendante

    Donnée : un schéma de traduction non récursif à gauche

    Résultat : le code d'un traducteur descendant

    Pour chaque non-terminal A, construire une fonction dont les paramètres sont les attributs hérités de A et qui renvoie comme valeur les attributs synthétisés de A (on suppose qu'il n'y en a qu'un). Le code pour A décide quelle règle appliquer en fonction du symbole courant dans la donnée. Pour chaque attribut d'une variable du membre droit, déclarer une variable locale.

    Le code associé à une règle parcourt le membre droit et fait les actions suivantes :

    • Pour un symbole terminal X avec un attribut x, sauvegarder la valeur de x dans une variable locale et lire X.
    • Pour un non-terminal B, faire c := B(b1, b2, ... bk) où c est l'attribut synthétisé de B et b1, b2, ... bk sont les attributs hérités de B.
    • Pour une action, faire l'action.

    Le problème est de calculer les attributs hérités, on effectue les actions seulement au moment où on réduit.

    Dans une règle A --> X1 X2 ...Xn, au moment où on passe Xi, on a dans la pile X1 X2 ...Xi-1 mais pas A.

    • Si un attribut hérité de Xi dépend d'un attribut de A, quand et comment le calculer ? on ira le chercher dans la pile et non dans la règle. La méthode présentée est applicable à certaines grammaires L-attribuées dont la grammaire sous-jacente est LR(1).




  • Chapitre 08 : Contrôle de type

    chap8

    8.1 Les systèmes de typage

    Les systèmes de typage font partie de l'analyse sémantique, plus précisément du contrôle statique qui englobe, le contrôle de type, le contrôle du flot d’exécution, et le contrôle d’unicité ou de répétition.

    • Le contrôle de type : Incompatibilité de type entre opérandes.
    • Le contrôle du flot d’exécution : Transfert du contrôle en un autre point du programme.
    • Le contrôle d’unicité ou de répétition : Un nom peut devoir apparaître un nombre fixé de fois.

    Le contrôle et la manipulation des types est une partie intermédiaire entre l'analyse syntaxique et la génération de code intermédiaire et il font partie des spécifications du langage.

    type

    Exemple d'un contrôle de type simple

    8.2 L'équivalence des types

    L'équivalence des types permet de traiter le problème quand deux expressions de type sont-elles égales. Pour faire l’équivalence de types, on utilise les arbres abstraits dont les feuilles sont des types de base. ces arbres permet de spécifier les interactions avec la représentation des types. Il existe deux types d'équivalence de type: structurelle et nominale.

    • Équivalence structurelle : Egalité des expressions, définie récursivement. Ne permet pas de rendre compte des cycles. Peut être affaiblie (en ne tenant pas compte des dimensions des tableaux passés en paramètres, par exemple.)

    Exemple:

    function equiv(s,t) : boolean ;

    begin

    if s et t sont du même type de base then equiv := true

    else if s = s1´ s2 and t = t1´ t2 then

    equiv := (equiv(s1 ,t1) and equiv(s2 ,t2))

    else if s = array(s1,s2 ) and t = array(t1,t2 ) then

    equiv := equiv(s2 ,t2)

    et ainsi de suite.

    On ignore ici l’ensemble des indices pour l’équivalence structurelle des tableaux.

    • Équivalence nominale : Identité des noms de type.

    Exemple:

    On peut souvent nommer les types :

    type lien = ­ cellule ;

    var suivant : lien ;

    dernier : lien ;

    p : ­ cellule ;

    q, r : ­ cellule ;

    Les variables suivant, dernier, p, q et r ont-elles le même type ? Cela dépend du type d’équivalence. Pas de réponse unique dans la norme PASCAL.

    Diverses possibilités : Structurellement, toutes sont équivalentes.

    En équivalence par nom, suivant et dernier sont équivalentes, ainsi que p, q et r ; par contre, p et suivant ne le sont pas. Mais de nombreuses implémentations de PASCAL créent des noms de types implicites lors des déclarations. L'implémentation classique est faite au moyen d'un graphe de type. On crée un nœud pour chaque constructeur et chaque type de base, et une feuille pour chaque nom. Dans l'exemple, cela donne :

    equi

    8.3 La manipulation des types

    La manipulation des types est une conversion de type nécessaire dans de nombreuses situations, par exemple x + i lorsque le premier opérande est un réel et le second un entier. Ces conversions peuvent être explicites ou implicites.

    • Explicites : Application d'une fonction, comme ord en PASCAL.
    • Implicites : Coercition, par exemple entier vers réel. Dans ce dernier cas, il y a souvent surcharge des opérateurs.
    Dans l'exemple, cela peut être pris en charge simplement au niveau du schéma de traduction :

    E→ E+E 

    [E.val, E.type] := if E1.type = real and E2.type = integer then [realadd(E1.val,inttoreal(E2.val)), real] ...........

    La conversion des constantes peut se faire à la compilation pour le  gain de temps à l'exécution. Dans l'exemple, le symbole + était surchargé. On peut parfois résoudre la surcharge en examinant les opérandes (cas précédent). C'est parfois impossible.

    Exemple de manipulation des types

  • Chapitre 10 : Génération de code

    chap10

    La génération de code intermédiaire peut être considérée comme le point ou on passe de la partie analyse à la partie synthèse. En principe, il reste peu ou pas de traces du langage source à ce point. Aussi, il y apparaît peu ou pas de traces du langage (ou de la machine) cible. Bien qu’optionnel, le passage par le code intermédiaire apporte certains avantages: 

    • Il est relativement aisé de changer le langage cible du compilateur.
    • La représentation intermédiaire permet d’appliquer des optimisations indépendantes du langage cible.

    generation

    Le générateur de code: 

    • Doit générer du code correct.
    • Devrait produire du code de bonne qualité.
    • Devrait être efficace.
    • Ne peut pas espérer générer un code optimal.

    L’entrée fournie au générateur de code: 

    • Représentation intermédiaire du programme source et informations stockées dans la table des symboles. 
    • La représentation peut prendre la forme de: – suite linéaire d’opérations et d’opérandes en notation postfixe.
    – Instructions à trois adresses.

    – Instructions d’une machine à pile.

    – Arbres (ou DAG) de syntaxe. 

    • Devrait avoir été analysée lexicalement, syntaxiquement et sémantiquement. 
    • Devrait être dotée d’opérations de conversion de types et de détection d’erreurs dynamiques.
    10.1 Considérations liées au design du générateur de code

    La forme du programme cible est normalement une des suivantes: 

    • Langage machine à adressage absolu.
    • Langage machine re-localisable.
    • Langage assembleur.
    • Langage pour une machine virtuelle 
    – Exemples : JVM (Java virtual machine), .NET/CLI, LLVM 

    – Généralement de plus haut niveau que le langage machine natif (fait abstraction du choix des registres, etc.) 

    • Autre langage de haut niveau. Le fait de produire du code re-localisable ou assembleur permet d’effectuer de la compilation par modules.

    Gestion de la mémoire pour les structures de données associées: 

    • A la table des symboles.
    • Aux étiquettes dans le code.
    • Aux noms des variables temporaires.
    • Aux informations produites par d’autres analyses de programmes.
    • Etc.
    10.2 Description de la machine cible utilisée 

    Machine à n registres avec des instructions “à trois adresses” de la forme générale: op destination, source1, source2, ou op est quelque chose comme MOV, ADD ou SUB. Divers modes d’adressage sont fournis:

    gene2

    Les instructions ont naturellement un coût relié à leur exécution. Par exemple, une division réelle vs. une addition entière. Par exemple, une instruction avec accès en mémoire vs. sans accès. Mais aussi, il y a un coût relié au chargement de l’instruction à partir de la mémoire. Par exemple, une instruction qui inclut une adresse absolue est plus longue et est donc plus couteuse à charger. Même chose pour une instruction qui inclut une constante littérale.

    10.3 Gestion du rangement dans le code cible

    Cette partie du cours, se concentre sur le code qu’il faut générer pour implanter la mécanique d’appel. On suppose qu’on a les instructions à trois adresses suivantes:
    1. call.
    2. return.
    3. halt et
    4. Action qui symbolise toute instruction qui n’est pas reliée à la mécanique d’appel.
    10.3.1 Allocation statique

    L’instruction intermédiaire call est transformée en les instructions machines suivantes:
    ST callee.staticArea,#here + 20
    BR callee.codeArea

    Ces instructions effectuent les actions suivantes:
    • Placer l’adresse de retour dans le bloc d’activation de la fonction appelée.(∗)
    • Passer le contrôle à l’appelé. 
    (∗) On suppose que le code à exécuter au retour se trouve juste après le BR et que la taille des instructions ST et BR est de 20 octets.
    L’instruction intermédiaire return est transformée en l’instruction machine BR *callee.staticArea.
    Celle-ci récupérer, dans le bloc d’activation de la fonction appelée, l’adresse de retour chez l’appelant.

    10.3.2 Allocation par pile

    L’instruction intermédiaire call est transformée en les instructions machines suivantes:
    ADD SP, SP, #caller.recordSize
    ST ∗SP, #here + 16
    BR callee.codeArea
    SUB SP, SP, #caller.recordSize
    Ces instructions effectuent les actions suivantes:
    • Faire sauter le pointeur de pile SP par-dessus le bloc d’activation de l’appelant. (On suppose que SP pointe toujours au début du bloc de la fonction en cours d’exécution.) 
    • Placer l’adresse de retour dans le bloc d’activation de la fonction appelée (à la première case du bloc). 
    • Passer le contrôle à l’appelé. 
    • [La dernière instruction fait partie du protocole de retour.]
    L’instruction intermédiaire return est transformée en l’instruction machine BR *0(SP). Cette instruction ainsi que la quatrième chez l’appelant effectuent les actions suivantes:
    • Récupérer l’adresse de retour dans le bloc d’activation courant (celui de l’appel´e) et passer le contrôle à l’appelant à l’endroit spécifié. 
    • Dépiler le bloc d’activation de l’appel´e en ramenant SP au début du bloc de l’appelant.

    10.4 Représentations intermédiaires

    On connait déjà les représentations intermédiaires suivantes: 

    • Les arbres de syntaxe (avec ou sans partage des sous-expressions communes).
    • La notation postfixe. 
    Exemple: Représentations de ‘a := (b + c) * (b + c) * (- d)’. 

    On introduit  la représentation du code à trois adresses: 

    • Il s’agit de séquences d’´énonces simples dont les plus courants sont de la forme ‘ x := y op z’ (2 opérandes + 1 receveur = 3 adresses).
    • Le découpage des expressions complexes du langage source en des séquences d’énoncés simples demande l’introduction de variables temporaires ti.
    10.2 Code à trois adresses

    Voici les types d’énoncés que l’on retrouve dans le code à trois adresses: 

    • ‘ x := y op z’ ou op est un opérateur binaire; 
    • ‘ x := op y’ ou` op est un opérateur unaire.
    • ‘ x := y’.
    • ‘goto L’, ou L est l’étiquette d’un énoncé dans la séquence.
    • ‘if x relop y goto L’ ou relop est un opérateur de comparaison comme ≤, >, 6=, etc.
    • ‘x := y[i]’ et ‘x[i] := y’ pour les accès dans des tableaux; 
    • ‘x := &y’, ‘x := *y’ et ‘*x := y’ pour la manipulation des pointeurs.
    • ‘L :’, pseudo-énoncé qui déclare l’étiquette L, il marque la position de l’énoncé suivant.

    Le choix du jeu d’instructions de la représentation intermédiaire doit être fait judicieusement. 

    • Il doit être assez riche pour pouvoir exprimer tous les calculs possibles dans le langage source. 
    • S’il est trop étendu, le générateur de code devient alors lourd; tout changement de langage cible est coûteux. 
    • S’il est trop restreint, les séquences de code intermédiaire à générer deviennent plus longues; la réalisation des optimisations subséquentes est plus difficile.

    10.2.1 Implantation des énoncés à 3 adresses 

    Pour le cours, nous nous en tenons à l’implantation par quadruplets. 

    • Permet plus de souplesse dans la réalisation d’optimisations. 
    • Les économies d’espace liées aux implantations par triplets ou triplets indirects ne valent plus tellement la peine de nos jours. 
    Exemple:

    generation2