Enseignants : Stéphane Talbot Miguel Tomasena Compilation Volume horaire : 10.5 C / 12 TD / 12 TP Contenu : Introduction à la compilation Analyse Lexicale Analyse Syntaxique Traduction dirigée par la syntaxe Analyse Sémantique Production de code Optimisation Traduction Programme en langage cible Programme en langage source 1 2 Compilation Références : Introduction à la compilation • Compilers : Principles, Techniques and Tools (dragon book). Alfred V. Aho, Ravi Sethi, Jeffrey. D. Ullman, Addison-Wesley, 2ème édition 2007. • Programming Language processors in Java. D. Watt & D. Brown, Prentice-Hall, 2000. Termes Phases d'un compilateur Importance des grammaires • Cours de compilation. Luc Maranget. Sur le Web. • JFlex - The Fast Scanner Generator for Java. Sur le Web. Méthodes d'analyse • CUP Parser Generator for Java. Sur le Web. 3 4 Termes Termes Terminologie : Compilateur : programme qui traduit d'un langage source vers un langage cible, en signalant d‘éventuelles erreurs La Syntaxe régit la forme, la structure d'une phrase. Exemple : if (3+5 ==9) print ("bravo ") Deux niveaux syntaxiques peuvent être cités : – Lexique : forme des symboles terminaux (les mots) – Syntaxique : forme d’une suite de terminaux La compilation concerne : • Langages informatiques • Architecture de machines • Théorie des langages • Algorithmique 5 La Sémantique régit la signification. Exemple d’erreur sémantique : if (3 == false) print ("bravo ") 6 Termes Termes Interpréteur : interpréter c’est parcourir un graphe (structure de données) dont les nœuds sont les instructions. Un interprète exécute lui même les instruction du programme. Compilateur : Un compilateur est un traducteur qui permet de transformer un programme écrit dans un langage L1 (source) en un autre programme écrit dans L2 (cible). L1 L2 peut être un langage intermédiaire, assembleur ou un langage d’une machine abstraite. Les langages interprétés sont souvent plus simples et flexibles que les langages compilés. Exemples de langages compilés : C, C++, ADA, Pascal, Java. Exemples de langages interprétées : JavaScript, PHP, BASIC, scheme, CaML, LISP, Perl, Python, Prolog. 7 L2 8 Termes Objectifs d'un Compilateur Le P-code concerne à la fois la compilation et l’interprétation Qu’attend-on d’un compilateur ? Le code source est traduit (compilé) dans une forme binaire compacte (pseudo-code ou p-code) qui n'est pas encore du code machine. Lors de l’exécution le P-code est interprété. La correction : le programme compilé doit représenter le même calcul que le programme original. Il est équivalent. Par exemple en Java le source est compilé pour obtenir un fichier (.class) "byte code" qui sera interprété par une machine virtuelle. L’efficacité : le compilateur doit produire un code qui s’exécutera aussi rapidement que possible. Les interpréteurs de P-code peuvent être relativement petits et rapides, le P-code peut s'exécuter presque aussi rapidement que du binaire. 9 10 Objectifs d'un Compilateur Ecriture d'un compilateur : Bootstrap Qu’attend-on d’un compilateur ? L’écriture d’un compilateur est une tâche complexe, le programmeur a intérêt à utiliser un langage de haut niveau. La détection des erreurs. Exemples d'erreurs : – Identificateurs mal formés, commentaires non fermés . . . – Constructions syntaxiques incorrectes – Identificateurs non déclarés – Expressions mal typées : if 3 then "toto" else 4.5 – Références non instanciées La plupart des compilateurs sont écrits en langage de haut niveau et non en assembleur. Si on ne dispose pas d’un langage de haut niveau, alors la stratégie de « programme amorce » (bootstrap) peut s’appliquer. Erreurs de Syntaxe. Erreurs détectées à la compilation, de nature statique. Erreurs d'exécution. Erreurs détectées à l’exécution, de nature dynamiques (division par zéro, dépassement des bornes d'un tableau, etc). 11 12 Compilation Bootstrap Stratégie de bootstrap. Si on veut un compilateur de L, on écrit d’abord un compilateur pour le sous-ensemble L’ avec l’assembleur M. T-diagramme schématisant un traducteur de L’ vers M réalisé en M : L’ M M Phases d'un compilateur Le compilateur ainsi réalisé servira à la construction d’un compilateur de L. L M L’ Résultat L M M L’ M M 13 14 Structure d’un compilateur Structure d’un compilateur Phases d'un compilateur : Programme en langage source Analyse lexicale • Analyse sémantique : analyse l’arbre de syntaxe abstraite pour calculer de nouvelles informations permettant de : – Rejeter des programmes incorrects (portée, typage. . . ) – Préparer la phase de génération de code (organisation de l’environnement, construction de la table de symboles, résolution de la surcharge . . . ) Programme en langage cible Analyse syntaxique Analyse sémantique Génération de code • Génération de code : transformation l'arbre syntaxique abstrait enrichi d'informations sémantiques en code machine ou assembleur. – Choix des instructions à émettre. – Allocation de registres : association entre les idéntifiants et les registres du processeur. – Optimisation. Gestion de la table des symboles 15 • Analyse lexicale : traduit une suite de caractères en suite de mots, tokens, ou unités lexicales. • Analyse syntaxique (ou grammaticale) : transforme une suite d'unités lexicales en un arbre représentant la structure de la Arbre de syntaxe abstraite : phrase. Structure intermédiaire avant l'analyse sémantique . Représente les dépendances sémantiques. 16 Phases de compilation, exemple Phases de compilation, exemple Exemple : Table de symboles. Le compilateur doit garder trace des noms de variables et les information correspondantes : • mémoire réservée, • type, • portée, • pour les sous-programmes : nombre et type des paramètres, mode de transmission de chacun, type du résultat. let p = i + v * 60 Analyse lexicale consiste à identifier les tokens ou unités lexicales. Token let position = initial + vitesse + 60 17 Unité lexicale mot clé identificateur (id1) let p = i + v * 60 affectation Table de symboles identificateur (id2) Adresse Symbole Informations identificateur (id3) id1 p int produit id2 i int entier id3 v int … … … addition 18 Phases de compilation, exemple Phases de compilation, exemple Analyse syntaxique (parser) vérifie que l’ordre des unités lexicales correspond au langage (à sa grammaire) et produit un arbre syntaxique (de dérivation). Arbre de dérivation :vs: arbre abstrait • L’arbre de dérivation produit par l’analyse syntaxique possède de nombreux nœuds superflus. La grammaire peut comporter de règles dont le but est de simplifier l’analyse syntaxique. let p = i + v * 60 expr let IDENT expr LET IDENT '=' expr expr = • Un arbre abstrait constitue une structure plus naturelle entre l’analyse syntaxique et l’analyse sémantique, il ne garde que les parties nécessaires. p opbin expr terme expr + expr opbin expr • L’arbre abstrait peut se construire lors de l’analyse syntaxique, en associant à toute règle de la grammaire une action sémantique. Ou bien à partir de l'arbre de dérivation. IDENT terme * nb i IDENT 60 v 19 20 Phases de compilation, exemple Arbre abstrait affectation Phases de compilation, exemple Génération de code intermédiaire. let p = i + v * 60 Description d'une machine à registres : • 8 registres (eax, ebx, ecx, edx, epb, esp, eflags, eip) : • eax, ebx, ecx, edx sont des registres généraux : • ebp et esp servent à gérer la pile (base et sommet de la pile) . • eflags registre d’état. • eip pointeur d’instruction. Les types sont cohérent + p * i i, vi,sont v sont desdéjà entiers précédemment initialisées (déclarés) déclarés v 60 Analyse sémantique : • Résolution des noms. Construction d'une table des symboles en associant des étiquettes aux identifiants. • Vérification de la cohérence de types. • Vérification des initialisations. Certains compilateurs signalent comme erreur la non-affectation initiale d'une 21 variable locale. • Instructions de la forme : • op dest,source • op dest • op • Qqs instructions : add, sub, mul, div, mov, push, pop, jmp, jl, etc. 22 Code généré, exemple : 23 DATA DATA SEGMENT SEGMENT p p DD DD i DD let p = i + v * 60 i v DD i DD v DD Instructions précédentes. DATA déclaration de données v ENDS DD On mets i dans la pile DATA CODE SEGMENT DATA ENDS ENDS * CODE SEGMENT ... CODE SEGMENT ... push ... eax,i mov mov eax,i eax,v eax,i v push push eax eax mov eax,v mov eax,60 push push eax eax multiplication mov pop eax,60 ebx push eax pop eax pop mul ebx eax,ebx pop pusheax eax mul eax,ebx push eax pop ebx pop eax affectation add eax, ebx push eax pop eax mov p, eax CODE ENDS Phases de compilation, exemple Optimisation. Il existe une grande diversité de types d'optimisation. Par exemple, dans le code précédent, il est inutile d'empiler pour dépiler par la suite : 60 . . . pop ebx pop eax add eax, ebx push eax pop eax mov p, eax . . . 24 . . pop pop add mov . . . ebx eax eax, ebx p, eax . Regroupement de phases Autre phases de compilation Partie frontale ou d’Analyse. Phases qui dépendent du langage source : lexicale, syntaxique. Préprocesseur. Transformations sur un code source, avant la compilation. Prise en compte de macros (#define) et des inclusions de fichiers (#include). Partie finale ou de Synthèse. Phases qui dépendent de la machine cible. Assembleur. Certain compilateurs produisent du code en langage d’assembleur. Il faut donc produire du code machine "translatable". Programme en langage source Partie frontale (analyse) code intermédiaire Partie finale (synthèse) Programme en langage cible Chargeur. Le code "translatable" est modifié à absolu et placé en mémoire aux emplacements appropriés. --> On peut être amené a écrire seulement une de ces deux parties. Relieur (éditeur des liens). Il permet de constituer un programme unique à partir de plusieurs fichiers contenant du code "translatable". 25 Passes : nombre de lectures du fichier source (une en général). Compromis entre nombre de passes et la mémoire nécessaire. 26 Compilation Traduction dirigée par la syntaxe Partie frontale d’un compilateur Description d’un langage source : • Syntaxe : grammaire non contextuelle. • Sémantique : descriptions plus ou moins formelles ou des exemples. Importance des grammaires Une grammaire non contextuelle permet de : • Spécifier la syntaxe • Guider la traduction : traduction dirigée par la syntaxe. Très utile pour organiser la partie frontale. La traduction dirigée par la syntaxe est une combinaison d’un analyseur syntaxique et d’un générateur de code intermédiaire. 27 28 Grammaire Expression suite de caractères Analyse lexicale suite d’unités lexicales Grammaire Traduction dirigée par la syntaxe Une phrase générée par la grammaire G est obtenue en partant du symbole initial et en appliquant des productions P jusqu'à l'obtention des terminaux. Représentation intermédiaire Le langage L(G) est l’ensemble de toutes les phrases que l’on peut générer avec G. Formellement, on écrit : L’ensemble de phrases syntaxiquement correctes d’un langage est décrit par une grammaire. * w} L(G) = { w Vt* | S0 Une grammaire est un quadruplet G = (VT,VN, S0, P) où 29 • VT est l’ensemble de symboles terminaux (unités lexicales), • VN est l’ensemble de symboles non terminaux, • S0 VN est l’axiome ou symbole de départ, • P est l’ensemble de productions (règles de grammaire) de la forme 30 Exemple • VT = {il, elle, est, boit, vite, beau} • VN = {<pronom>, <verbe>, <adj>, <phrase>} • S0 = <phrase> • P = { <phrase> <pronom> <verbe> <adj> , <pronom> il | elle , <verbe> est | boit , Rmq : certaines phrases <adj> vite | beau } n’ont pas de sens (il boit beau) Grammaire Grammaire et arbre syntaxique Exemple de notation : <bloc> begin <liste_opt_instr> end <liste_opt_instr> <liste_instr> | Chaîne vide <liste_instr> <liste_instr> ; <intr> | <intr> Hiérarchie des grammaires (Chomsky). type nom grammaire forme des règles structures produites exemple typique modèle équivalent 3 régulières ou rationnelle A→wB A→w peignes an automate fini non contextuelle ou algébrique A → arbres 2 anbn réseau de transition récursif anbncn existe mais compliqué Voir l'exemple précédant 1 contextuelle A→ 0 générale → où 31 quelconque dérivation :- application de plusieurs productions successives Arbre syntaxique (ou de dérivation) • Illustre la manière dont l’axiome se dérive en une chaîne du langage. • Représentation graphique d’une suite de dérivations machines de Turing A et B sont des non terminaux Exercice : écrire les grammaires et les automates correspondants w est un terminal , et sont des séquences de terminaux ou non terminaux et ≠ε 32 Propriétés d’un Arbre syntaxique : 1. La racine est étiquetée par l’axiome 2. Chaque feuille est étiquetée par un terminal (u.lexicale) ou par 3. Chaque nœud intérieur est étiqueté par un non-terminal 4. Si A est le non-terminal d’un nœud et ses fils sont X1, .., Xn alors A X1, ..,Xn est une production Grammaire et arbre syntaxique Grammaire et arbre syntaxique Grammaire ambiguë : s'il existe plus d'un arbre pour une même phrase. Règles pour résoudre l’ambiguïté : 1. Associativité des opérateurs : convention d'évaluation Dans la compilation on besoin de grammaires non ambigües (ou des règles pour résoudre l’ambigüité). On peut être amené à réécrire une grammaire pour éviter les ambigüités. Ex d'opérateurs : +, -, *, / Ex d'opérateurs : =, **, &&, || Règle associative à gauche : <expr> <expr> + <chiffre> | <chiffre> <chiffre> 0 | 1 | . . . | 9 Règle associative à droite : <affect> <lettre> = <affect> | <lettre> <lettre> a | b | c | . . . | z Associativité à gauche Exemple de grammaire ambiguë : <chaîne> <chaîne> + <chaîne> | <chaîne> - <chaîne> | 0|1|2|...|9 Arbres pour 9 – 5 + 2 <chaîne> <chaîne> 33 9 - + <chaîne> - <chaîne> <chaîne> + <chaîne> <chaîne> 2 9 5 <expr> <chaîne> 5 <affect> <expr> <expr> <chaîne> <chaîne> Associativité à droite <chiffre> 2 34 Grammaire et arbre syntaxique + + <chiffre> <chiffre> 2 5 <lettre> a = <affect> <lettre> = <affect> <lettre> b c 9 Grammaire et arbre syntaxique Règles pour résoudre l’ambiguïté : Démarche de la construction d'une grammaire non ambiguë des expressions arithmétiques : 2. Priorité des opérateurs : convention d'évaluation. 1. On définit une table synthétisant l'associativité et la priorité des opérateurs : 9 + 5 * 2 s'évalue 9 + (5 * 2) associativité à gauche : + - l'ordre correspond à la priorité associativité à gauche : * / On dit que * a une priorité supérieure à + car il 'applique avant. 2. On introduit deux non-terminaux <expr> et <terme> correspondant aux deux niveaux de priorité. 3. On introduit un autre non-terminal <factor> pour les unités de base (impossible à éclater) : chiffres et expressions parenthèses. 35 36 Résultat : <expr> <expr> + <terme> | <expr> - <terme> | <terme> <terme> <terme> * <factor> | <terme> / <factor> | <facteur> Arbre syntaxique <factor> <chiffre> | (<expr>) 9+5*2 ?? Grammaire et arbre syntaxique Grammaire et arbre syntaxique Exercice à faire en cours La grammaire précédente est ambiguë à cause du conditionnel. Exercice écrire une grammaire pour un sous-ensemble des instructions Java : <instr> et <instrs>. Remarques : • Considérez 7 types de <instr> : affectation, deux conditionnels, while, do while, bloc d'instructions et instruction vide ( ). La définition de <instr> repose sur : <instr>, <instrs>,<bool> et <expr> • Terminaux : id if else while do = ; ( ) { } • Le point-virgule doit apparaître à la fin des productions qui ne finissent pas par <instr>. • La définition de <instrs> (liste de instructions) repose sur : <instrs> et <instr> 37 <instr> if (<bool>) <instr> else <instr> | if (<bool>) <instr> | autres instructions L'instruction if (estX) if (estY) a=x; else a=y; admet deux arbres syntaxiques : <instr> if estX if ( <bool> )<instr> estX <instr> if ( <bool>)<instr> else <instr> estY 38 Traduction dirigée par la syntaxe Ré-écriture de la grammaire pour enlever l'ambiguïté : La traduction dirigée par la syntaxe est réalisée en attachant des règles (ou programmes) à des productions. <instr> <instr_close> | <instr_non_close> <instr_non_close> if (<bool>) <instr> | if (<bool>) <instr_close> else <instr_non_close> <instr_close> if (<bool>) <instr_close> else <instr_close> | autres instructions Exemple : <expr> <expr1> + <terme> traduire <expr> peut se faire avec le pseudo-code suivant : traduire expr1; reconnaître "+" traduire terme; traiter addition; L'instruction if (estX) if (estY) a=x; else a=y; admet un arbre syntaxique : <instr> <instr_non_close> if ( <bool> ) Le pseudo-code peut servir à construire un arbre abstrait pour expr, ou bien pour un traitement plus simple comme par exemple la traduction en postfix. <instr> <instr_close> estX if ( <bool> ) <instr_close> else <instr_close> estY 40 Traduction dirigée par la syntaxe Traduction dirigée par la syntaxe Un Attribut : information associée aux unités (nœuds) syntaxiques. Par exemple : chaîne de caractères; valeur, adresse mémoire, etc. Deux manières d'associer les règles ou actions sémantiques à la grammaire : 1. Définition dirigée par la syntaxe : chaque production est accompagné d'une règle sémantique. Une production peut avoir un ensemble de règles ou actions sémantiques pour calculer la valeur des attributs. 2. Schémas de traduction : on insère des instruction (programmes) à l'intérieur des productions. On parle d'actions sémantiques. X.a dénote la valeur de l'attribut a du nœud X. On dit que l'attribut est synthétisé quand la valeur remonte (est calculée) à partir des attributs fils. On parle d'attribut hérité dans le cas contraire, quand la valeur provienne des nœuds ancêtres. 41 <instr> else <instr> estY Grammaire et arbre syntaxique 39 ( <bool> ) <instr> if ( <bool> ) 42 Traduction dirigée par la syntaxe Traduction dirigée par la syntaxe 1. Définition dirigée par la syntaxe. Chaque production est accompagné d'une règle sémantique. concaténation Exemple traduire une expression en postfixe : Production Règle sémantique L'arbre "décoré" est l'arbre syntaxique représentant explicitement les valeurs des attributs pour chaque nœud. L'arbre "décoré" correspondant à l'analyse de 9 + 2 * 4 <expr> <expr1> + <terme> expr.t = expr1.t + terme.t + "+" <expr> <expr1> - <terme> <expr> <terme> expr.t = expr1.t + terme.t + "-" <terme> <terme1> * <factor> <terme> <terme1> / <factor> terme.t = terme1.t + facteur.t + "*" terme.t = "9" terme.t = "2" * terme.t = terme1.t + facteur.t + "/" facteur.t = "9" facteur.t = "2" <terme> <factor> terme.t = facteur.t <factor> (<expr>) <factor> 0 facteur.t = expr.t expr.t = "9 2 4 * +" expr.t = terme.t facteur.t = "9" facteur.t = "0" facteur.t = "1" <factor> 1 ... 43 44 Traduction dirigée par la syntaxe 2. Schémas de traduction. On insère des programmes (actions sémantiques) à l'intérieur des productions. <expr> <expr1> - <terme> {print("-")} <expr> <terme> <terme> <terme1> * <factor> {print("*")} <terme> <terme1> / <factor> {print("/")} <terme> <factor> <factor> (<expr>) <factor> 0 {print("0")} Arbre syntaxique correspondant au schéma de traduction : reste {print("+")} <factor> 1 {print("1")} ... reste1 45 Les règles sémantiques ont été évaluées en faisant un parcours en profondeur d'abord. On évalue après avoir visité tous les fils. Les attributs sont synthétisés <expr> <expr1> + <terme> {print("+")} + <terme> {print("+")} <reste1> terme 2 Production action sémantique + 4 Traduction dirigée par la syntaxe Par la suite on utilisera des schémas de traduction. <reste> --> facteur.t = "4" Schémas de traduction pour traduire vers postfixe : Similaire à une définition dirigée par la syntaxe, excepté que l'ordre d'évaluation des actions sémantiques est donné explicitement. Exemple : 9 terme.t = "2 4 *" + expr.t = "9" terme.t = terme1.t + facteur.t + "*" 46 Compilation Appliquez le schéma de traduction à l'expression : 9 + 2 * 4 Analyse syntaxique descendante Deux méthodes d'analyse : • Descendante : programmation facile, on part de l'axiome (racine) • Ascendante : s'applique à des grammaires plus générales. Des outils logiciels tendent à utiliser des méthodes ascendantes. Méthodes d'analyse Analyse descendante. On commence par l'axiome, en réalisant de manière répétitive les deux étapes suivantes : 1. Au nœud n, étiqueté par un non-terminal A, choisir une production de A et construire les fils de n avec les symboles de la partie droite de la production. Choisir selon le symbole de prévision 2 . Choisir le prochain nœud où un sous-arbre doit être construit. 47 48 Choisir le nœud le plus à gauche : dérivation gauche Analyse syntaxique descendante Analyse syntaxique descendante Exemple d'application de l'analyse descendante : Exemple de chaîne d'entrée : while (i<=6) a=a+1; Sous-ensemble d'instructions de Java. Au départ, le terminal while est le symbole de pré-vision. <instr> id = <expr> ; | if (<expr>) <instr> | while (<bool>) <instr> | ... Entrée while (i<=6) a=a+1; Arbre <inst> <instr> <inst> while ( while ( 49 Si le symbole de pré-vision nous signale une seule production possible, alors on peut appliquer une analyse syntaxique prédictive sans besoin de retour-arrière. ou Analyse Descendante Déterministe 50 Analyse syntaxique prédictive • • Une procédure est associée à chaque non terminal. Le symbole de pré-vision (preVis) permet de sélectionner de manière définitive la production à appliquer. Pseudo-code : <instr> id = <expr> ; | 51 if (<expr>) <instr> | public void instr() { while (<bool>) <instr> |<expr> ; | switch (preVis){ ... case ID : accepter(ID); accepter(AFFECT); expr( ); accepter(PV); break; case IF : accepter(IF); accepter(PG); expr(); accepter(PD); instr(); break; case WHILE : accepter(WHILE); accepter(PG); bool(); accepter(PD); instr(); break; case : . . . break; // autres instructions case default : print("erreur syntaxe"); ID, AFFECT, PV, IF, PG, PD, WHILE } sont des terminaux, des unités lexicales } Analyse syntaxique prédictive S'il existe plus d'une production pour un nœud A, par exemple A et A . On choisi d'appliquer A si : 1. a PREMIER() où a est le symbole de pré-vision et 2. PREMIER() PREMIER() = Associativité Récursivité à gauche. Notez la production suivante gauche <expr> <expr> + <terme> Une analyse syntaxique prédictive ne peut pas s'appliquer à une telle production. Dans ce cas il faut ré-écrire la production pour éliminer la récursivité à gauche. L'analyse syntaxique prédictive sera abordée plus en détail dans le chapitre Analyse Syntaxique 53 <intr> <instr> <instr> while (<bool>) <instr> Pour certaines grammaires, les étapes peuvent être appliquées sans avoir besoin de faire des retour-arrière (de manière déterministe). Au cours de l'analyse, le terminal d'entrée courant est appelé symbole de pré-vision. <bool> ) <bool> ) Analyse syntaxique prédictive Suite pseudo-code : public void accepter(Terminal t) { if (preVis == t) preVis = uniteSuivante(); // appel analyse lexical else print("erreur syntaxe"); } Définition PREMIER() = ensemble des terminaux pouvant apparaître au début d’une dérivation de . Exemples : PREMIER(instr) = {id, if, while, …} PREMIER(expr) = {id, chiffre, . ..} 52