Compilateur Compilation Un compilateur est un programme Laurent Braud 1 Département d’Informatique Laboratoire d’Informatique Fondamentale, Luminy 2 [email protected] pageperso.lif.univ-mrs.fr/~laurent.braud/compilation/ Groupe Cours, TD TP 1 Laurent Braud Laurent Braud 2 Janusz Malinowski Compilateur / interpréteur compilateurinterpréteur programme cible Le compilateur signale de plus toute erreur contenue dans le programme source Lorsque le programme cible est un programme exécutable, en langage machine, l’utilisateur peut ensuite le faire exécuter afin de traiter des données et de produire des résultats. Exemple programme source données qui lit un autre programme rédigé dans un langage de programmation, appelé langage source et qui le traduit dans un autre langage, le langage cible. résultat Un interpréteur est un programme qui effectue lui-même les opérations spécifiées par le programme source directement sur les données fournies par l’utilisateur. programme source programme cible program p; var d:integer; f: pop pop push add function f(a, b : integer) sw : integer sw var pop c, k : integer push begin jra k := a + b; main: li f := k; sw end; push li begin push d := 7; jla write(f(d, 2) + 1); pop end. ... $t0 $t1 $ra $t2, $t0, $t1 $t2, k $t2, f $ra $t2 $t0, 7 $t0, d $t0 $t1, 2 $t2 f $t2 Nécessité d’une analyse syntaxique A l’exception de quelques cas (rares), la traduction ne peut être faite “mot à mot” Le programme source doit être décomposé en composants pertinents ou constructions du langage source. La traduction d’une construction dépend de la position qu’elle occupe au sein du programme. Deux parties d’un compilateur Décomposition d’une définition de fonction function f (a, b : integer) : integer var c, k : integer begin k := a + b; f := k; end; (* (* (* (* (* (* (* (* (* (* nom de la fonction *) parametres *) type de retour *) debut de la decl. des variables *) decl. des variables locales *) debut du corps de la fonction *) affectation *) expression arithmetique *) instruction de retour *) fin du corps de la fonction *) Deux parties d’un compilateur programme source La traduction du programme source en programme cible se décompose en deux étapes : L’analyse, réalisée par la partie frontale du compilateur, qui découpe le programme source en ses constituants ; détecte des erreurs de syntaxe ou de sémantique ; produit une représentation intermédiaire du programme source ; conserve dans une table des symboles diverses informations sur les procédures et variables du programme source. syntaxe représentation intermédiaire La synthèse, réalisée par la partie finale du compilateur, quit construit le programme cible à partir de la représentation intermédiaire et de la table des symboles sémantique programme cible Nature de la représentation intermédiaire Portabilité La conception d’une bonne RI est un compromis : Elle doit être raisonnablement facile à produire à partir du programme source. Un compilateur est portable s’il permet de générer du code pour plusieurs processeurs. Elle doit être raisonnablement facile à traduire vers le langage cible. Le recours à une RI permet de produire un compilateur portable en ne multipliant que le générateur de code à partir de la RI. Elle doit donc être raisonnablement éloignée (ou raisonnablement proche) du langage source et du langage cible. Portabilité Syntaxe du langage source programme source Le programme source vérifie un certain nombre de contraintes syntaxiques. syntaxe représentation intermédiaire L’ensemble de ces contraintes est appelé grammaire du langage source. Si le programme ne respecte pas la grammaire du langage, il est considéré incorrect et le processus de compilation échoue. sémantique sémantique sémantique assembleur x86 assembleur SPARC assembleur 68K Description de la grammaire du langage source Grammaires formelles Littéraire Un programme est une suite de définitions de fonction Une définition de fonction est composée du nom de la fonction suivie de ses arguments suivie de la declaration de ses variables internes suivie d’un bloc d’instructions Une instruction est . . . Formelle programme listeDecFonc listeDecFonc decFonc → → → → ... listeDecFonc ’.’ decFonc listeDecFonc Les contraintes syntaxiques sont représentées sous la forme de règles de réécriture. La règle A → BC nous dit que le symbole A peut se réécrire comme la suite des deux symboles B et C. L’ensemble des règles de réécriture constitue la grammaire du langage. La grammaire d’un langage L permet de générer tous les programmes corrects écrits en L et seulement ceux-ci ID FCT listeParam listeDecVar ’ ;’ instrBloc Notations et Terminologie Dans la règle A → α A est appelé partie gauche de la règle. α est appelé partie droite de la règle. Lorsque plusieurs règles partagent la même partie gauche : A → α1 , A → α2 , . . . , A → α n On les note : A → α1 | α2 | . . . | α n Grammaire partielle des expressions arithmétiques EXPRESSION OP2 EXPRESSION EXPRESSION NOMBRE CHIFFRE → → → → → → EXPRESSION OP2 EXPRESSION +|-|*|/ NOMBRE ( EXPRESSION ) CHIFFRE | CHIFFRE NOMBRE 0|1|2|3|4|5|6|7|8|9 Les symboles EXPRESSION, OP2, NOMBRE, CHIFFRE sont appelés symboles non terminaux de la grammaire Les symboles +, -, *, /, (, ), 0, 1 , ..., 9 sont appelés symboles terminaux de la grammaire Avantages des grammaires formelles Dérivation d’une expression arithmétique Une grammaire formelle : Pousse le concepteur d’un langage à en décrire la syntaxe de manière exhaustive. Permet de répondre automatiquement à la question mon programme est-il correct ? à l’aide d’un analyseur syntaxique. Fournit, à l’issue de l’analyse, une représentation explicite de l’organisation du programme en constructions (structure syntaxique du programme). Cette représentation est utile pour la suite du processus de compilation. Arbre de dérivation L’expression arithmétique 2 ∗ (3 + 1) est-elle correcte ? EXPRESSION ⇒ EXPRESSION OP2 EXPRESSION ⇒ NOMBRE OP2 EXPRESSION ⇒ CHIFFRE OP2 EXPRESSION ⇒ 2 OP2 EXPRESSION ⇒ 2 * EXPRESSION ⇒ 2 * ( EXPRESSION ) ⇒ 2 * ( EXPRESSION OP2 EXPRESSION) ⇒ 2 * ( NOMBRE OP2 EXPRESSION) ⇒ 2 * ( CHIFFRE OP2 EXPRESSION) ⇒ 2 * ( 3 OP2 EXPRESSION) ⇒ 2 * ( 3 + EXPRESSION) ⇒ 2 * ( 3 + NOMBRE) ⇒ 2 * ( 3 + CHIFFRE) ⇒ 2 * ( 3 + 1) Analyse lexicale EXPRESSION EXPRESSION OP2 NOMBRE * EXPRESSION ( CHIFFRE 2 EXPRESSION ) EXPRESSION Afin de simplifier la grammaire décrivant un langage, on omet de cette dernière la génération de certaines parties simples du langage. Ces dernières sont prises en charge par un analyseur lexical EXPRESSION OP2 EXPRESSION NOMBRE + NOMBRE CHIFFRE CHIFFRE 3 4 L’analyseur lexical traite le programme source et fournit le résultat de son traitement à l’analyseur syntaxique. Nouvelle grammaire des expressions arithmétiques Analyseur lexical Lit le programme source Syntaxe Lexique EXPRESSION EXPRESSION EXPRESSION OP2 NOMBRE CHIFFRE → → → → → → EXPRESSION OP2 EXPRESSION NOMBRE ( EXPRESSION ) +|-|*|/ CHIFFRE | CHIFFRE NOMBRE 0|1|2|3|4|5|6|7|8|9 Reconnaı̂t des séquences de caractères significatives appelées lexèmes Pour chaque lexème, l’analyseur lexical émet un couple (type du lexème, valeur du lexème) Exemple La nouvelle grammaire omet les détails de la génération d’un NOMBRE et d’un opérateur binaire. Cette partie est à la charge de l’analyseur lexical. La frontière entre analyse lexicale et analyse syntaxique est en partie arbitraire. Analyseur syntaxique plus simple NOMBRE, 2 OP2,* EXPRESSION PF, ) EXPRESSION EXPRESSION NOMBRE, 3 OP2,+ Les symboles terminaux de la grammaire (ou types de lexèmes) constituent l’interface entre l’analyseur lexical et l’analyseur syntaxique. Ils doivent être connus des deux. La traduction dirigée par la syntaxe est réalisée en attachant des actions sémantiques aux règles de la grammaire. Elle repose sur deux concepts : EXPRESSION PO, ( Les types de lexèmes sont des symboles, ils constituent les symboles terminaux de la grammaire du langage. Traduction dirigée par la syntaxe EXPRESSION EXPRESSION (NOMBRE, 123) EXPRESSION NOMBRE, 4 Les attributs. Un attribut est une quantité quelconque associée à une construction du langage de programmation. Exemples : le type d’une expression la valeur d’une expression le nombre d’instructions dans le code généré Les constructions étant représentées par les symboles de la grammaire, on associe les attributs à ces derniers. Notations : A.t est l’attribut t associé au symbole A. Traduction dirigée par la syntaxe Traduction — exemple Les schémas de traduction sont une notation permettant d’attacher des fragments de programme aux règles de la grammaire. Les fragments sont exécutés quand la production est utilisée lors de l’analyse syntaxique. Le résultat combiné des exécutions de tous ces fragments, dans l’ordre induit par l’analyse syntaxique, produit la traduction du programme auquel ce processus est appliqué. E E E O O N N C C C C Traduction — Exemple O (*) N (2) * action sémantique = E1 .t || E2 .t || O.t = E1 .t = N.t = + = − = C.t || N2 .t = C.t = 0 = 1 = 2 ... C.t = 9 E.t E.t E.t O.t O.t N.t N.t C.t C.t C.t Attributs synthétisés E (2 3 1 + *) E (2) règle → EOE → (E) → N → + → − → CN → C → 0 → 1 → 2 ... → 9 Un attribut est dit synthétisé si sa valeur au niveau d’un nœud A d’un arbre d’analyse est déterminée par les valeurs de cet attribut au niveau des fils de A et de A lui même. E (3 1 +) ( E (3 1 +) A ) A.t = f ( B.t, C.t, D.t) B C (2) E (3) O (+) E (1) 2 N (3) + N (1) C (3) C (1) 3 1 C D Les attributs synthétisés peuvent être évalués au cours d’un parcours ascendant de l’arbre de dérivation. Un tel parcours peut être effectué simultanément à la construction de l’arbre (lors de l’analyse syntaxique). Dans l’exemple, B.t, C.t et D.t doivent être calculés avant de calculer A.t Table des symboles Table de symboles — exemple programme source Elle rassemble toutes les informations utiles concernant les variables et les fonctions ou procédures du programme. Pour toute variable, elle garde l’information de : son nom son type sa portée son adresse en mémoire Pour toute fonction ou procédure, elle garde l’information de : son nom sa portée le nom et le type de ses arguments, ainsi que leur mode de passage éventuellement le type du résultat qu’elle fournit La table des symboles est construite lors de l’analyse syntaxique. Arbre de dérivation v/s arbre abstrait L’arbre de dérivation produit par l’analyse syntaxique possède de nombreux nœuds superflus, qui ne véhiculent pas d’information. De plus, la mise au point d’une grammaire nécessite souvent l’introduction de règles dont le seul but est de simplifier l’analyse syntaxique. Un arbre abstrait constitue une interface plus naturelle entre l’analyse syntaxique et l’analyse sémantique, elle ne garde de la structure syntaxique que les parties nécessaires à l’analyse sémantique et à la production de code. L’arbre abstrait est construit lors de l’analyse syntaxique, en associant à toute règle de grammaire une action sémantique. program p; var d:integer; table des symboles function f(a, b : integer) : integer var c, k : integer begin k := a + b; f := k; end; id f a b c k main d classe fonct param param locale locale fonct locale type entier entier entier entier entier entier entier adr 1 args 2 11 0 begin d := 7; write(f(d, 2) + 1); end. Arbre de dérivation v/s arbre abstrait * EXPRESSION EXPRESSION OP2,* EXPRESSION NOMBRE, 2 PO, ( EXPRESSION PF, ) EXPRESSION EXPRESSION NOMBRE, 3 OP2,+ + 2 EXPRESSION NOMBRE, 4 3 1 Analyse sémantique Représentation intermédiaire L’analyse sémantique utilise l’arbre abstrait, ainsi que la table de symboles afin d’effectuer un certain nombre de contrôles sémantiques, parmi lesquels : vérifier que les variables utilisées ont bien été déclarées. le contrôle de type : le compilateur vérifie que les opérandes d’un opérateurs possèdent bien le bon type. conversions automatiques de types. Exemple La production de code est effectuée lors du parcours de la Représentation intermédiaire (RI) construit à l’issue le l’étape d’analyse. Plusieurs choix sont possibles pour la RI : l’arbre abstrait une séquence d’instructions élémentaires ... Code à 3 adresses Production de code lors du parcours d’un nœud associé à une instruction si expr alors instr. si arbre représentant expr La production de code consiste à produire une séquence d’instructions sémantiquement équivalente au programme source qui pourra être interprétée par une machine donnée. ⇒ arbre représentant instr calculer expr le ranger dans x et le rangeant dans x SIFAUX x ALLER A suite exécuter instr suite : Avant d’écrire tout-à-fait le code, on peut passer par une version simplifiée, où l’on ne se préoccupe pas des problèmes de la table de symboles. boucle: t1 = n - 1 while i < n-1 do SI i > t1 ALLER A suite begin t2 = i * 2 i := i * 2 + 1; i = t1 + 1 end ALLER A boucle suite: Sources A.Aho, M.Lam, R.Sethi et J.Ullman, Compilateurs : principes, techniques et outils. 2ème édition. Pearson Education, 2007 A.Appel, Modern compiler implementation in C. Cambridge University Press, 1998 Henri Garreta, Polycopié du cours de compilation. Projet Construction en langage C d’un compilateur. Le langage source est une version simplifiée d’un langage impératif bien connu, le Pascal. Le langage cible est l’assembleur MIPS. Il est interprété par une machine virtuelle appelée spim. http://henri.garreta.perso.luminy.univmed.fr/Polys/PolyCompil.pdf Pascal Pascal — instructions Langage classique, connu pour sa simplicité et sa rigueur. Types : notre Pascal connaı̂t trois types : deux types simples : le type entier et les booléens. un type dérivé : les tableaux d’entiers. Opérateurs : le Pascal reconnaı̂t les opérateurs suivants : arithmétiques : + , −, ∗, /, mod comparaison : =, <, <=, <> logique : not, and, or Le langage reconnaı̂t les instructions suivantes : Instruction vide Bloc d’instructions, délimité par un begin et end Affectation a := b + 1 (contrairement à C, une affectation n’est pas une expression, elle ne correspond pas à une valeur ; on ne peut écrire a := (b := 4)) Instruction if ... then begin ... end et sa variante ... else begin ... end Instruction while ... do begin ... end Pascal — sous-programmes Sous-programmes : il y a deux types de sous-programmes : les fonctions et les procédures. Leurs arguments sont simples (des entiers). Le passage se fait par valeur. Elles possèdent des variables locales. Un sous-programme ne peut pas être déclaré à l’interieur d’un autre. Les procédures ne renvoient rien. Les fonctions renvoient toujours un résultat entier. Elles disposent d’une variable locale supplémentaire qui a le même nom que la fonction, dans laquelle on stocke le résultat. Procédures prédéfinies : Les entrées-sorties de valeurs entières se font à l’aide de la fonction read et de la procédure write, toutes deux prédéfinies : a := read(); write(a); Assembleur MIPS Langage utilisé par le processeur MIPS. On utilisera la machine virtuelle spim. Mémoire séparée en trois parties : l’espace global (variables) la zone de code, la pile. En plus, registres utilisés dans la plupart des instructions. registres courants : $t0-$t9, pointeur de sommet de pile : $sp, adresse de retour de saut : $ra ... Pascal — exemple program p; var d:integer; (* debut du programme *) (* decl. des variables globales *) function f(a, b : integer) (* decl. d’une fonction a 2 args *) : integer (* type de retour *) var (* decl. des variables locales *) c, k : integer begin (* debut du corps de la fonction *) k := a + b; (* affect. et expression arithmetique *) f := k; (* affectation de la valeur de retour *) end; (* fin du corps *) begin d := f(d, 2); write(d + 1); end. (* debut du corps du programme *) (* appel de fonction, affectation *) (* appel de la procedure predefinie *) (* fin du programme *) MIPS — exemples lw $t0, k li $t1, 8 add $t2, $t0, $t1 jump label charge la variable à l’adresse k dans $t0 charge la valeur 8 dans $t1 addition : $t2 :=$t0 + $t1 saute à l’adresse label dans le code Structure du compilateur Etapes programme source analyse lexicale syntaxe analyse syntaxique arbre abstrait table de symboles analyse sémantique code à 3 adresses données assembleur résultats TP 1 2 3 4 5 6 durée 1 2 1 1 2 2 intitulé analyseur lexical analyseur syntaxique production de l’arbre abstrait code à 3 adresses table des symboles, production de code utilisation des outils lex et yacc