Compilation & langages formels · L3 · Section 7/8
Annales corrigées
Progression
#Annales corrigées — Compilation
Les exercices qui suivent sont tirés des sujets d'examen de l'UE Compilation de L3 Informatique (Université Nice Sophia Antipolis, puis Université Côte d'Azur), dont le cours et les TD sont assurés par Sid Touati. Trois sujets sont archivés localement:
- Annale_2015-2016.pdf — examen de session 1, mai 2016, 3 h: analyse lexicale (5 pts), analyse sémantique et typage (8 pts), génération de code optimal par Sethi-Ullman (7 pts).
- Annale_Session2_2015-2016.pdf — examen de rattrapage, juin 2016, 2 h 30: mêmes trois parties, mêmes barèmes.
- Annale_Session2_2017-2018.pdf — examen de rattrapage, juin 2018, 2 h 30: traduction dirigée par la syntaxe (8 pts), Sethi-Ullman (7 pts), expressions postfixées (5 pts).
Deux consignes reviennent sur tous les sujets: justifiez précisément vos réponses, et seule une feuille A4 manuscrite recto-verso est autorisée. Autrement dit, le cours doit être su, pas feuilleté: les exercices demandent de construire (une description lex, une action sémantique, un code trois adresses), pas de restituer.
Ces sujets ne recoupent pas le corpus OFI du semestre 3, qui porte sur les langages formels, le dénombrement et les récurrences. Les prérequis propres à l'UE Compilation — outil lex, outil yacc, traduction dirigée par la syntaxe, algorithme de Sethi-Ullman — n'apparaissent nulle part ailleurs dans le fonds documentaire.
#Exercice 1 — Lexer un fichier de log HTTP
Source: Annale_2015-2016.pdf, exercice 1 (5 points); Annale_Session2_2015-2016.pdf, exercice 1 (5 points).
On veut collecter des statistiques d'accès aux pages web personnelles des utilisateurs à partir du fichier /var/log/access.log, dont un extrait a la forme suivante:
1193.51.25.77 - - [12/Jul/2004:13:44:46 +0200] "GET /~hpc/ HTTP/1.0" 200 3822 "-" "Mozilla/4.73 [en] (X11; I; FreeBSD 3.4-RELEASE i386)"2193.51.25.77 - - [12/Jul/2004:13:47:57 +0200] "GET /~hpc/ HTTP/1.0" 304 - "-" "Mozilla/4.73 [en] (X11; I; FreeBSD 3.4-RELEASE i386)"3193.51.24.15 - - [09/Dec/2004:11:20:27 +0200] "POST /~dupont/manuals/InsertInfo.php HTTP/1.0" 200 2321 "http://vlaminck.labo.univ.fr/~dupont/manuals/InsertInfo.php" "Opera/5.0 (Linux 2.4.2 i386; U) [en]"Il y a un accès HTTP par ligne. La ligne est composée de: l'adresse IP de la machine qui se connecte; deux entiers positifs ou tirets; la date de connexion; la méthode de connexion HTTP (GET, POST, PUT) suivie de la page accédée et du protocole utilisé (HTTP suivi d'un numéro de version), le tout entre guillemets; deux entiers positifs ou tirets; l'adresse de la page demandée; le nom du navigateur web suivi de sa version.
Les pages personnelles sont toutes de la forme ~login/repertoires, le login étant composé d'au plus 8 lettres.
Question. Écrivez avec l'outil lex un programme qui extrait, pour chaque requête, le login s'il s'agit d'une page personnelle (NULL sinon), le nom du navigateur (sans sa version) et l'adresse IP de la machine. Le programme doit compter et afficher: le nombre d'accès à la page personnelle pour chaque login; le nombre de requêtes par navigateur; le nombre de requêtes provenant de chaque adresse IP. Il est conseillé d'utiliser une ou plusieurs tables de symboles.
Correction détaillée
Méthode. Un analyseur lexical est un automate fini: il n'a aucune mémoire non bornée. Or l'énoncé demande trois comptages indexés par des chaînes arbitraires (login, navigateur, IP). La solution tient donc en deux parties: le lexer reconnaît et extrait les champs (travail fini, faisable par un AFD), et une table de symboles externe fait office de mémoire associative (travail de comptage, hors du modèle fini). C'est exactement la frontière du cours entre reconnaissance et mémorisation.
Pourquoi des conditions de démarrage. Le lexer ne peut pas décider avec une seule règle globale: la ligne contient deux champs entre guillemets (le référent et le navigateur) qui se ressemblent, et le premier doit être ignoré. Il faut donc savoir où l'on en est dans la ligne. Les conditions de démarrage (%x) donnent cet état supplémentaire sans sortir du modèle fini — elles se compilent en dupliquant l'automate par contexte. Ici, cinq états suffisent: INITIAL (avant l'IP), CHAMPS (identité, date, requête), NUM (les deux champs numériques), REFERENT (le champ à ignorer), AGENT (le navigateur).
1%{2#include <stdio.h>3#include <stdlib.h>4#include <string.h>5 6/* ---- table de symboles : association chaine -> compteur ---- */7typedef struct { char *cle; int valeur; } entree;8typedef struct { entree *t; int n, cap; } table_t;9 10static void table_init(table_t *T) { T->t = NULL; T->n = 0; T->cap = 0; }11 12/* Renvoie un pointeur sur le compteur associe a `cle`, en le creant si besoin. */13static int *table_acces(table_t *T, const char *cle) {14 int i;Points de rédaction qui rapportent des points.
- Deux champs entre guillemets, un seul à retenir. C'est le piège central de l'exercice. Le référent et le navigateur ont la même forme lexicale (une chaîne entre guillemets), et le référent vient en premier. Une règle unique
<CHAMPS>\"{NAV}[^"\n]*\"attraperait le référent"http://vlaminck/"(qui commence par une lettre) et n'atteindrait jamais le navigateur: le compteurnavserait faux, sans aucun message d'erreur. Il faut donc compter les champs: deux champs numériques après la requête, puis le référent, puis le navigateur. C'est à cela que servent les étatsNUM,REFERENTetAGENT. - Pourquoi une condition de démarrage et pas une règle plus précise. On pourrait écrire une regex qui capture la ligne entière d'un coup. Mais l'énoncé demande de commenter les structures de données et les fonctions utilisées: la découpe en champs par états est ce qui rend le programme lisible et testable champ par champ. C'est aussi le style attendu dans un vrai lexer, où l'on garde un état par contexte syntaxique.
- Le nom sans la version. Le séparateur est le
/deMozilla/4.73: on copie jusqu'au premier/(ou espace, ou fin de champ). Tester par préfixe serait faux (OperaetOperaMinisont deux navigateurs distincts). - Extraction du login. L'énoncé impose au plus 8 lettres. La borne
k < 8dans la boucle de copie matérialise cette contrainte; sans elle, un chemin/~abcdefghijkl/produirait un login de 12 caractères, et la statistique serait indexée sur une clé que l'énoncé n'autorise pas. - Une table de symboles, pas trois compteurs. L'énoncé dit « une ou plusieurs tables de symboles »: la clé est une chaîne quelconque, l'accès doit être dynamique. Une recherche linéaire suffit pour un TP; en production on passerait à une table de hachage, mais l'interface (
table_accesrenvoyant unint *) ne change pas. - Un détail du fonds local. Dans la copie PDF archivée du sujet, le champ navigateur des lignes 7 et 9 se lit
0pera/5.0et0pera/7.0— avec un zéro initial, et non la lettreO. Si le fichier de log contient réellement ce caractère, la classe[A-Za-z]de la règleNAVne matche pas le champ, et le navigateur est compté à tort. Il faut décider explicitement: soit le log est propre (Opera), soit la règle doit accepter un premier caractère quelconque hors espace et hors guillemet. Sur un sujet d'examen, signaler ce cas rapporte des points; le passer sous silence le fait perdre. - Compilation.
flex access.l && cc lex.yy.c -lfl -o stats && ./stats < /var/log/access.log.
Vérification. Faites tourner le programme sur l'extrait de l'énoncé (9 lignes) et contrôlez chaque table séparément:
- IP:
193.51.25.77→ 6 (lignes 1 à 6),193.51.24.15→ 3 (lignes 7 à 9); - login (pages personnelles seulement):
hpc→ 2 (lignes 1 et 2),touati→ 1 (ligne 3),dupont→ 2 (lignes 7 et 8),samual→ 1 (ligne 9); - navigateur:
Mozilla→ 6 (lignes 1, 2, 4, 5, 6 et 8),Opera→ 2 (lignes 7 et 9),IE→ 1 (ligne 3).
Un écart sur une seule de ces valeurs localise l'erreur. Les deux causes les plus fréquentes: le référent compté comme navigateur (état REFERENT oublié), et un login tronqué par une borne mal placée. Le total des trois tables navigateur doit faire 9, comme celui des IP.
#Exercice 2 — Représenter les types d'un mini-C
Source: Annale_2015-2016.pdf, question 2.1 (1,5 pt); Annale_Session2_2015-2016.pdf, question 2.1.
On veut réaliser un vérificateur de type pour un langage mini C. La grammaire yacc considérée pour les déclarations est:
1declaration : basic_type declarator ;2basic_type : INT | FLOAT | VOID ;3declarator : '*' direct_declarator | direct_declarator ;4direct_declarator : IDENTIFIER5 | '(' declarator ')'6 | direct_declarator '(' parameter_list ')'7 | direct_declarator '(' ')' ;8parameter_list : parameter parameter_list | parameter ;9parameter : declaration ;Question. Proposez une structure C pour décrire les types déclarables avec cette grammaire. Représentez avec cette structure les déclarations float p(float a, int *b) et float *foo(float (*hook)(float a), int b).
Correction détaillée
Méthode: lire la grammaire comme un constructeur de types. Chaque non-terminal correspond à une opération de construction de type:
basic_typeproduit un type de base (INT,FLOAT,VOID);declarator : '*' direct_declaratorproduit un pointeur vers le type engendré pardirect_declarator;direct_declarator : IDENTIFIERproduit un type « encore inconnu », porteur d'un nom;direct_declarator : direct_declarator '(' parameter_list ')'produit une fonction dont le type de retour est celui dudirect_declaratorde gauche et dont les paramètres sont ceux de droite;direct_declarator : '(' declarator ')'ne construit rien: c'est un groupement, il ne sert qu'à fixer la lecture.
Un type est donc soit un atome (type de base), soit un constructeur à un argument (pointeur), soit un constructeur à deux arguments (fonction: retour + liste de paramètres). D'où une structure récursive:
1typedef enum { BASE_INT, BASE_FLOAT, BASE_VOID } base_t;2typedef enum { GENRE_BASE, GENRE_PTR, GENRE_FUNC } genre_t;3 4typedef struct type_s type_t;5 6typedef struct param_s {7 char *nom; /* le nom du paramètre, pour les messages d'erreur */8 type_t *type; /* son type */9 struct param_s *suiv; /* liste chaînée: l'ordre des paramètres compte */10} param_t;11 12struct type_s {13 genre_t genre; /* BASE, PTR ou FUNC */14 base_t base; /* si genre == GENRE_BASE */Pourquoi cette forme et pas une autre. Un type est un arbre, pas une chaîne de caractères: int * et int ** sont deux types différents, int (*f)(void) et int *f(void) aussi. Le champ genre est le discriminant (l'équivalent d'un tag de variante); cible porte l'information structurelle; params porte la liste ordonnée des paramètres. Les types de base sont au fond de la récursion, ce qui explique la règle declaration : basic_type declarator de la grammaire: la base n'est connue qu'à la toute fin de l'analyse, on la « bouche » alors dans le trou laissé par la feuille IDENTIFIER.
Déclaration float p(float a, int *b). On lit: p est une fonction retournant float, prenant un float nommé a et un pointeur vers int nommé b.
1FUNC cible = BASE(FLOAT)2 params ─┬─ { nom = "a", type = BASE(FLOAT) }3 └─ { nom = "b", type = PTR( cible = BASE(INT) ) } ─── suivant = NULLSous forme d'initialisation C:
1type_t *t_a = type_base(BASE_FLOAT);2type_t *t_b = type_ptr(type_base(BASE_INT));3param_t *pa = cons_param("a", t_a, NULL);4param_t *pb = cons_param("b", t_b, pa);5type_t *t_p = type_fonction(type_base(BASE_FLOAT), pb);Déclaration float *foo(float (*hook)(float a), int b). C'est le cas qui piège: foo est une fonction qui retourne un pointeur sur float, et dont le premier paramètre est un pointeur sur fonction. On lit de l'intérieur vers l'extérieur, en suivant la règle de la « spirale »:
1FUNC cible = PTR( cible = BASE(FLOAT) ) ← foo retourne float *2 params ─┬─ { nom = "hook",3 │ type = PTR( cible = FUNC cible = BASE(FLOAT)4 │ params = { nom = "a", type = BASE(FLOAT) } ) }5 └─ { nom = "b", type = BASE(INT) }Autrement dit: hook n'est pas une fonction, c'est un pointeur vers une fonction. Le GENRE_FUNC n'est jamais atteint directement depuis un paramètre; il est toujours derrière un GENRE_PTR. Cette distinction est précisément ce que vérifie la question 2.5 (voir exercice 5).
Erreur classique. Représenter le type par une chaîne ("float*") et compter les *. Cela marche sur les exemples simples et échoue dès qu'un pointeur de fonction apparaît: float (*hook)(float) et float *hook(float) s'écrivent presque pareil et désignent deux choses différentes. Le type doit être un arbre.
#Exercice 3 — Attributs yacc et prototypes de la table des symboles
Source: Annale_2015-2016.pdf, questions 2.2 et 2.3 (2 pts au total).
Question 2.2. Donnez les types des attributs à déclarer dans le %union pour construire les structures de type.
Question 2.3. Proposez les prototypes des fonctions permettant de stocker le type d'un identificateur dans une table des symboles, de tester si un identificateur y figure déjà, et de lire le type d'un identificateur déjà présent. Le type de la table est supposé défini et s'appelle table_t.
Correction détaillée
2.2 — le %union. Les attributs transportés par la grammaire sont exactement les valeurs produites par les règles:
basic_typetransporte un type de base (base_t), pas untype_t *: à ce stade on ne sait encore rien de la structure;declaration,declarator,direct_declaratortransportent un arbre de type (type_t *);parameter_listetparametertransportent une liste de paramètres (param_t *);IDENTIFIERtransporte une chaîne (le nom), etINT/FLOAT/VOIDsont des mots-clés sans valeur, donc sans attribut.
1%union {2 base_t base; /* basic_type */3 type_t *type; /* declaration, declarator, direct_declarator */4 param_t *params; /* parameter_list, parameter */5 char *nom; /* IDENTIFIER */6}7 8%token <nom> IDENTIFIER9%token <base> INT FLOAT VOID10 11%type <base> basic_type12%type <type> declaration declarator direct_declarator13%type <params> parameter_list parameterDeux remarques de rédaction. D'une part, basic_type n'est pas de type type_t *: mélanger les deux obligerait à allouer un nœud pour chaque mot-clé et rendrait la règle declaration inutilement coûteuse. D'autre part, les terminaux INT, FLOAT, VOID portent bien un attribut <base>: c'est l'action de basic_type qui le lit, comme le montre l'exercice suivant.
2.3 — l'interface de la table des symboles. Trois opérations suffisent, et il faut les typer correctement: la table est hétérogène du point de vue de l'appelant (elle rend un type ou un booléen), d'où trois signatures distinctes plutôt qu'une fonction polymorphe.
1/* Insère (nom, type) si absent. Renvoie 1 si l'insertion a eu lieu,2 0 si l'identificateur était déjà déclaré. */3int table_stocker (table_t *t, const char *nom, type_t *type);4 5/* Teste la présence d'un identificateur. */6int table_contient(table_t *t, const char *nom);7 8/* Lit le type associé à un identificateur déjà présent.9 Précondition: table_contient(t, nom) est vrai. */10type_t *table_lire (table_t *t, const char *nom);Le point à justifier: table_stocker renvoie un entier, non void. La question 2.5 demande justement de détecter les redéclarations, et c'est ce code de retour qui le permet sans parcourir la table une seconde fois.
#Exercice 4 — Actions sémantiques yacc
Source: Annale_2015-2016.pdf, question 2.4 (3 pts); Annale_Session2_2015-2016.pdf, question 2.4.
Question. Écrivez les actions sémantiques yacc pour chaque règle syntaxique, permettant de remplir la table des symboles.
Correction détaillée
Le point délicat: le type de base arrive après le déclarateur. La règle est declaration : basic_type declarator. En analyse ascendante (yacc), declarator est réduit avant basic_type: au moment où l'on construit le nœud de declarator, la base est encore inconnue. La solution standard est de laisser un trou dans la feuille IDENTIFIER, puis de le boucher au moment de la réduction de declaration. On ajoute donc un champ type_t *base_a_boucher (ou, plus simplement, on mémorise le nom de l'identificateur dans une variable globale pendant la réduction de direct_declarator : IDENTIFIER, ce qui suffit ici puisque la grammaire n'a qu'une déclaration en cours à la fois).
1/* Règle 1 : la base comble le trou laissé par la feuille IDENTIFIER. */2declaration3 : basic_type declarator4 { $$ = $2; type_boucher_base($2, $1); }5 ;6 7basic_type8 : INT { $$ = BASE_INT; }9 | FLOAT { $$ = BASE_FLOAT; }10 | VOID { $$ = BASE_VOID; }11 ;12 13/* '*' devant un déclarateur = un pointeur vers ce déclarateur. */14declaratorJustifications à écrire sur la copie.
declarator : '*' direct_declaratorenveloppe le type de droite dans unGENRE_PTR. L'ordre est contre-intuitif: dansfloat *foo(...), le*est à gauche defoomais c'est le type de retour defooqui devient un pointeur, pasfoolui-même. C'esttype_fonction($1, $3)qui construitFUNC(cible = $1), et$1vaut alorsPTR(FLOAT): la lecture est correcte.direct_declarator : '(' declarator ')'ne fait rien d'autre que propager$2. Les parenthèses ne créent pas de type: elles forcent seulement la dérivation, exactement comme dans les expressions arithmétiques où(a+b)*ceta+b*cont des arbres différents mais les mêmes nœuds.parameter : declarationréutilise la règle complète, ce qui autorise récursivement les pointeurs de fonction en paramètre — c'est ce qui rendfloat (*hook)(float a)analysable.- La table des symboles n'est pas remplie ici: elle l'est dans
declarator_list(exercice suivant), car c'est seulement là qu'on connaît le nom complet et qu'on peut détecter les collisions.
#Exercice 5 — Vérifications d'erreurs: redéclaration et fonctions imbriquées
Source: Annale_2015-2016.pdf, question 2.5 (1,5 pt); Annale_Session2_2015-2016.pdf, question 2.5.
On ajoute la règle declarator_list : declarator_list declarator | declarator.
Question. Écrivez les actions sémantiques de cette règle qui effectuent les vérifications suivantes: un identificateur ne peut pas être déclaré deux fois; les fonctions ne peuvent pas retourner une fonction ni prendre une fonction en argument.
Correction détaillée
1declarator_list2 : declarator_list declarator { enregistrer($2); }3 | declarator { enregistrer($1); }4 ;avec
1static void enregistrer(type_t *t)2{3 char *nom = declarateur_nom(t);4 5 /* (1) redéclaration ? */6 if (table_contient(&table, nom)) {7 erreur("identificateur %s deja declare", nom);8 return; /* on n'ecrase pas l'ancien type */9 }10 11 /* (2) fonction retournant une fonction, ou fonction en parametre ? */12 if (contient_fonction_imbriquee(t)) {13 erreur("%s : une fonction ne peut ni retourner ni prendre une fonction", nom);14 return;et le parcours qui implémente la vérification (2):
1/* Renvoie 1 si le type contient une fonction « nue » la ou c'est interdit. */2static int contient_fonction_imbriquee(const type_t *t)3{4 if (!t || t->genre == GENRE_BASE) return 0;5 6 if (t->genre == GENRE_PTR) /* un pointeur sur fonction est licite */7 return contient_fonction_imbriquee(t->cible);8 9 /* genre == GENRE_FUNC */10 if (t->cible && t->cible->genre == GENRE_FUNC) /* retour = fonction : interdit */11 return 1;12 13 for (param_t *p = t->params; p; p = p->suiv)14 if (p->type && p->type->genre == GENRE_FUNC) /* parametre = fonction nue : interdit */Pourquoi la récursion sur GENRE_PTR est indispensable. C'est le cœur de l'exercice. float *foo(float (*hook)(float a), int b) est valide: hook est un paramètre dont le type est PTR(FUNC(...)), et un pointeur sur fonction est une valeur parfaitement légitime en C. En revanche float f(float g(float)) — un paramètre de type FUNC(...) sans pointeur — est invalide: on ne peut pas passer une fonction « nue » en argument. La fonction de vérification doit donc s'arrêter (et renvoyer 0) dès qu'elle traverse un GENRE_PTR, et ne signaler l'erreur que sur un GENRE_FUNC atteint sans pointeur intermédiaire.
Formulation générale du C réel. Cette contrainte n'est pas propre à l'exercice: la norme C interdit les types fonction retournant fonction et fonction prenant une fonction en paramètre, mais autorise les pointeurs correspondants. C'est pourquoi la grammaire de déclarateurs impose le passage par declarator (qui peut produire un *) pour les paramètres de type fonction.
Erreur classique. Écrire if (t->cible->genre == GENRE_FUNC) return 1; sans distinguer GENRE_PTR de GENRE_FUNC en tête de fonction: la vérification rejette alors float (*hook)(float), qui est licite. Tester la correction sur les deux déclarations de l'exercice 2 est le moyen le plus rapide de détecter ce bug.
#Exercice 6 — Sethi-Ullman: arbre, registres, code optimal
Source: Annale_2015-2016.pdf, questions 3.1 à 3.3 (5 pts); Annale_Session2_2015-2016.pdf, questions 3.1 à 3.3; Annale_Session2_2017-2018.pdf, questions 3.1 à 3.3.
On utilise l'algorithme de Sethi-Ullman vu en cours et en TD pour générer un code optimal (trois adresses) pour l'expression arithmétique:
Questions. 3.1 Dessinez l'arbre binaire abstrait. 3.2 Décorez l'arbre pour déduire le nombre minimal de registres nécessaires à un code optimal sans spill. 3.3 Donnez le code trois adresses optimal.
Correction détaillée
3.1 — l'arbre. Attention aux parenthèses et aux priorités. Les opérateurs * et / sont plus prioritaires que + et -, et tous sont associatifs à gauche. En appliquant ces règles, la structure de la partie gauche est (((a+b)*(c+d))/(a-c))*(b+d) puis + a puis + (b*e), et la racine de l'expression entière est le * extérieur:
1* (racine)2├── +3│ ├── +4│ │ ├── *5│ │ │ ├── /6│ │ │ │ ├── *7│ │ │ │ │ ├── + (a+b)8│ │ │ │ │ └── + (c+d)9│ │ │ │ └── - (a-c)10│ │ │ └── + (b+d)11│ │ └── a12│ └── * (b*e)13└── /14 ├── *3.2 — les nombres de Sethi-Ullman (nombres d'Ershov). Règle de décoration: une feuille reçoit le nombre 1; pour un nœud interne dont les sous-arbres gauche et droit portent les nombres et ,
Le nombre obtenu à la racine est le nombre minimal de registres nécessaires pour évaluer l'expression sans code de vidage. Intuition: si les deux sous-arbres demandent le même nombre de registres, il faut en plus conserver le résultat du premier pendant l'évaluation du second; si l'un en demande strictement moins, ce résultat « tient » dans les registres déjà occupés par le plus gourmand.
Décoration, de bas en haut:
1* [4]2├── + [3]3│ ├── + [3]4│ │ ├── * [3]5│ │ │ ├── / [3]6│ │ │ │ ├── * [3] ← 2,2 ⇒ 37│ │ │ │ │ ├── + [2] (a+b)8│ │ │ │ │ └── + [2] (c+d)9│ │ │ │ └── - [2] (a-c)10│ │ │ └── + [2] (b+d)11│ │ └── a [1]12│ └── * [2] (b*e)13└── / [3]14 ├── * [3] ← 2,2 ⇒ 3À la racine, les deux fils portent , donc le nombre est .
Vérification de deux nœuds au passage: (a+b)*(c+d) a deux fils de nombre 2 ⇒ 3. ((a+b)*(c+d))/(a-c) a des fils de nombres 3 et 2, donc : la division n'exige pas de registre supplémentaire, le sous-arbre gauche gardant la main.
3.3 — code trois adresses optimal (4 registres). La règle de génération découle de la décoration: on évalue d'abord le fils de plus grand nombre, puis l'autre, puis on combine. Si , le sous-arbre droit est évalué avec un registre de moins, puisque le registre du résultat gauche est déjà occupé.
1; ---- partie gauche : (a+b)*(c+d)/(a-c)*(b+d) + a + b*e ----2R0 = a3R1 = b4R0 = R0 + R1 ; R0 = a+b5R1 = c6R2 = d7R1 = R1 + R2 ; R1 = c+d8R0 = R0 * R1 ; R0 = (a+b)*(c+d)9R1 = a10R2 = c11R1 = R1 - R2 ; R1 = a-c12R0 = R0 / R1 ; R0 = (a+b)*(c+d)/(a-c)13R1 = b14R2 = d33 instructions, 4 registres, aucun spill. Le nombre de registres effectivement utilisés simultanément ne dépasse jamais 4, et il atteint 4 exactement dans la partie droite (R1, R2, R3 occupés plus R0 qui conserve la partie gauche): c'est le nœud racine qui impose la borne.
Point de méthode. Le code optimal n'est pas « le plus court »: il est celui qui minimise le nombre de registres simultanément vivants, et Sethi-Ullman est prouvé optimal pour les arbres sans re-matérialisation. Deux erreurs classiques: évaluer systématiquement le fils gauche en premier (ici c'est correct à la racine, mais faux dès que , cas qui se produit dans la partie droite si l'on écrit la division dans l'autre sens), et oublier qu'un nœud dont les deux fils ont des nombres différents ne coûte pas de registre supplémentaire.
#Exercice 7 — Sethi-Ullman avec un nombre borné de registres
Source: Annale_2015-2016.pdf, question 3.4 (2 pts); Annale_Session2_2015-2016.pdf, question 3.4.
Question. On suppose que le nombre de registres disponibles est égal à trois. Générez un code optimal de l'expression de l'exercice 6, qui contient éventuellement du code de vidage (spill).
Correction détaillée
Le nombre d'Ershov de la racine vaut 4: avec 3 registres, au moins un spill est inévitable, et un seul suffit. La stratégie découle de l'arbre: les deux sous-arbres de la racine demandent chacun 3 registres. On évalue donc entièrement la partie gauche avec les 3 registres disponibles, on la vide en mémoire, puis on évalue la partie droite — qui tient dans 3 registres — et on recharge pour combiner.
1; ---- partie gauche : 3 registres suffisent (Ershov = 3) ----2R0 = a3R1 = b4R0 = R0 + R15R1 = c6R2 = d7R1 = R1 + R28R0 = R0 * R19R1 = a10R2 = c11R1 = R1 - R212R0 = R0 / R113R1 = b14R2 = dBilan: 35 instructions (33 + 1 écriture mémoire + 1 lecture), 2 accès mémoire de spill, 3 registres. Le coût est minimal: le spill est placé à la frontière de la racine, c'est-à-dire à l'endroit exact où la pression sur les registres atteint 4.
Pourquoi ce spill est optimal. Le nombre d'Ershov de la racine vaut 4, donc toute allocation à 3 registres doit vider au moins une valeur; une seule valeur est suffisante, car les deux sous-arbres de la racine ont un nombre d'Ershov de 3 et tiennent donc chacun dans les 3 registres. Placer le spill ailleurs (par exemple à l'intérieur de la partie droite, où le nœud * a deux fils de nombre 2) n'apporterait rien et coûterait un accès mémoire supplémentaire.
Vérification. Le résultat doit être identique au code à 4 registres sur un jeu de valeurs: prendre , , , , , , , et exécuter les deux codes à la main. Toute divergence signale un spill mal placé (typiquement une valeur écrasée entre l'écriture et la relecture).
#Exercice 8 — Traduction dirigée par la syntaxe: chemins dans le plan
Source: Annale_Session2_2017-2018.pdf, exercice 1 (8 points).
Un point du plan, identifié par ses coordonnées cartésiennes , part de et effectue des mouvements élémentaires d'une unité vers le haut (), le bas (), la gauche () et la droite (). Un chemin est une séquence de mouvements écrite comme une expression arithmétique: B + G signifie « un mouvement vers le bas suivi d'un mouvement vers la gauche »; l'opérateur indique une répétition, 3 × G valant « trois mouvements vers la gauche ». L'opérateur est plus prioritaire que (donc 3 × G + B se lit (3 × G) + B), et les parenthèses permettent de répéter un chemin complexe: 3 × (H + 7 × (B + 3 × G)).
La grammaire suivante, d'axiome , engendre ces expressions:
Questions. 1.1 Montrez que est ambiguë. 1.2 Dessinez l'arbre de dérivation de 3 × (H + 5 × B). 1.3 Écrivez une grammaire non ambiguë telle que . Puis, pour chaque question suivante, écrivez une traduction dirigée par la syntaxe sur (pas nécessairement en yacc), en définissant les attributs et en précisant pour chacun s'il est hérité ou synthétisé. 1.4 Combien de mouvements élémentaires comporte un chemin? 1.5 Quelles sont les coordonnées du point à l'issue du chemin? 1.6 Quelle est l'abscisse maximale de la trajectoire suivie?
Correction détaillée
1.1 — est ambiguë. La règle est doublement récursive à droite et à gauche, et elle est la seule à traiter : le mot H + B + G admet deux arbres de dérivation distincts, correspondant à et à . Il suffit d'exhiber ces deux arbres pour conclure — c'est ce qu'on attend, pas un argument de comptage.
1 E E2 /|\ /|\3 E + E E + E4/|\ | | /|\5H + B G H B + GSémantiquement les deux arbres donnent le même chemin (l'addition des déplacements est associative), mais la grammaire est ambiguë au sens syntaxique: un mot a plusieurs arbres. C'est un défaut de la grammaire, pas du langage.
1.2 — arbre de dérivation de 3 × (H + 5 × B).
1 E2 /|\3 N × E4 | |5 C (E)6 | /|\7 3 E + E8 | \9 H N × E10 | |11 C B12 |13 51.3 — grammaire non ambiguë . L'ambiguïté vient de ce qu'une seule règle traite à la fois les opérateurs de priorités différentes. On la supprime en stratifiant la grammaire, exactement comme pour les expressions arithmétiques du cours:
Justification de la non-ambiguïté, en trois arguments:
- Un est une suite de séparés par des
+, et un ne contient jamais de+au niveau supérieur: la décomposition deH + B + Gen est donc unique (le premier+non parenthésé). La récursion à droite fixe l'associativité à droite, sans créer de choix. - Un est soit un facteur, soit
N × Toù est une suite de chiffres. Comme est maximal (la règle force à absorber tous les chiffres), le découpageN × ...est unique. - est atomique: aucun choix interne.
On vérifie sur les deux cas qui départagent et : 3 × G + B se lit (3 × G) + B (la partie s'arrête avant le +), et 3 × (H + 5 × B) exige les parenthèses pour répéter une somme — ce qui est bien la sémantique annoncée par l'énoncé.
1.4 — nombre de mouvements élémentaires. Attribut n, synthétisé sur , , , plus un attribut v (valeur entière du nombre) synthétisé sur et .
| Production | Action sémantique |
|---|---|
E.n = T.n + E1.n | |
E.n = T.n | |
T.n = N.v * T1.n | |
T.n = F.n | |
F.n = 1 | |
F.n = E.n | |
N.v = N1.v * 10 + C.v | |
N.v = C.v | |
| chiffre | C.v = d |
Tous les attributs sont synthétisés: aucune information ne descend de la racine vers les feuilles. Sur l'exemple de l'énoncé, 3 × (4 × G + D) donne .
1.5 — coordonnées finales. Deux attributs dx et dy, synthétisés sur , , .
| Production | Action sémantique |
|---|---|
E.dx = T.dx + E1.dx ; E.dy = T.dy + E1.dy | |
E.dx = T.dx ; E.dy = T.dy | |
T.dx = N.v * T1.dx ; T.dy = N.v * T1.dy | |
T.dx = F.dx ; T.dy = F.dy | |
F.dx = 0 ; F.dy = 1 | |
F.dx = 0 ; F.dy = -1 | |
F.dx = -1 ; F.dy = 0 | |
F.dx = 1 ; F.dy = 0 | |
F.dx = E.dx ; F.dy = E.dy |
Sur G + H + D + B: , conforme à l'énoncé. La répétition fois se traite par une simple multiplication, ce qui est bien la sémantique de N × E.
1.6 — abscisse maximale. C'est la question qui distingue les étudiants: le maximum dépend de où l'on commence, donc il semble exiger un attribut hérité (l'abscisse courante). Une solution plus élégante n'utilise que des attributs synthétisés, en transportant le déplacement:
dx: déplacement horizontal total du sous-chemin (synthétisé);mx: abscisse maximale relativement au point de départ du sous-chemin (synthétisé).
| Production | Action sémantique |
|---|---|
E.dx = T.dx + E1.dx ; E.mx = max(T.mx, T.dx + E1.mx) | |
E.dx = T.dx ; E.mx = T.mx | |
T.dx = N.v * T1.dx ; T.mx = T1.mx + max(0, (N.v - 1) * T1.dx) | |
T.dx = F.dx ; T.mx = F.mx | |
F.dx = 0 ; F.mx = 0 | |
F.dx = 0 ; F.mx = 0 | |
F.dx = -1 ; F.mx = 0 | |
F.dx = 1 ; F.mx = 1 | |
F.dx = E.dx ; F.mx = E.mx |
Trois points à justifier:
- Concaténation. La trajectoire de parcourt d'abord (maximum relatif
T.mx) puis décalé deT.dx(maximum relatifT.dx + E1.mx). Le maximum est doncmax(T.mx, T.dx + E1.mx). - Répétition. copies de la même unité démarrent aux abscisses . Chaque copie atteint au plus relativement à son départ, donc le maximum global est : si la dernière copie est la plus à droite, sinon c'est la première.
- Cas de base. Pour
G, la trajectoire visite puis : l'abscisse maximale est , pas . PourD, elle vaut . C'est le point où l'on oublie facilement de compter le point de départ.
Sur l'exemple D + H + G + B de l'énoncé: donne (dx=1, mx=1); la queue donne dx = 0, mx = 0; d'où mx = max(1, 1+0) = 1, conforme à l'énoncé. Aucun attribut hérité n'est nécessaire — c'est la raison pour laquelle on transporte le déplacement.
#Exercice 9 — Expressions postfixées: grammaire et traduction
Source: Annale_Session2_2017-2018.pdf, exercice 3 (5 points).
Une expression est en notation infixée si les opérateurs sont entre leurs opérandes; elle est en notation postfixée si les opérateurs suivent leurs opérandes: 3 + 9 devient 3 9 +, et (45 + 37) × 22 devient 45 37 + 22 ×. La frontière entre opérandes numériques est marquée par le caractère □: l'expression précédente s'écrit 22□45□37 + ×.
Questions. 3.1 Écrivez en notation postfixée (3 + 5) × (6 + 7) et 3 + (5 × 6) + 7. 3.2 Écrivez une grammaire non ambiguë engendrant les expressions postfixées à opérandes entiers, sur l'alphabet terminal . 3.3 En associant à chaque symbole un attribut s (chaîne de caractères) donnant la représentation infixée, écrivez les actions sémantiques de .
Correction détaillée
3.1 — traduction. Un parcours post-ordre de l'arbre: opérandes d'abord, opérateur ensuite.
(3 + 5) × (6 + 7)devient3□5 + 6□7 + ×.3 + (5 × 6) + 7: l'associativité à gauche donne(3 + (5 × 6)) + 7, donc3□5□6□× + 7□ +.
3.2 — grammaire non ambiguë. Le point clé: une expression postfixée valide est un mot où tout préfixe strict contient plus d'opérandes que d'opérateurs, et le mot entier contient exactement un opérande de plus que d'opérateurs. Cette propriété se traduit directement en grammaire, en distinguant « un nombre » de « une expression complète »:
Justification de la non-ambiguïté — c'est le cœur de la question:
- est non ambigu:
N → C N | C □place le□en dernière position, et la lecture d'un nombre est donc déterminée par la position de son marqueur de fin. - est non ambigu: dans un mot postfixé valide, le dernier symbole est nécessairement l'opérateur racine, et le découpage
E Eest alors unique. En effet le premier symbole est un nombre; soit la longueur du plus court préfixe qui soit une expression complète (il existe, et il est unique puisqu'un préfixe valide se termine par un opérateur ou un□). Alors est exactement ce préfixe, et est le reste. Aucun autre découpage ne peut fonctionner, car tout autre choix laisserait un des deux facteurs invalide.
Autrement dit, la notation postfixée est un code uniquement décodable: la position des opérateurs détermine l'arbre sans aucune convention de priorité. C'est précisément pour cette raison que les compilateurs l'utilisent comme représentation intermédiaire avant la génération de code à pile (voir la page Génération de code).
3.3 — actions sémantiques. Attribut s, synthétisé, de type chaîne, sur , et . On produit une forme infixée entièrement parenthésée, ce qui garantit que la chaîne obtenue est elle-même non ambiguë.
| Production | Action sémantique |
|---|---|
E.s = N.s | |
E.s = "(" + E1.s + O.s + E2.s + ")" | |
N.s = C.s | |
N.s = C.s + N1.s | |
| chiffre | C.s = d |
Sur 22□45□37 + ×: la sous-expression 45□37 + produit "(45+37)", puis l'expression complète produit "(22(45+37)×)". Si l'on veut la forme infixée minimale (sans parenthèses superflues), il faut un attribut supplémentaire de priorité, et les actions deviennent conditionnelles — c'est le sujet classique de la « jolie impression » d'un arbre. La forme parenthésée est celle attendue ici, car elle est correcte sans information supplémentaire.
Vérification. La chaîne produite doit se reparser en une expression infixée équivalente. Sur 3□5□6□× + 7□ +, les actions donnent "((3(5×6))+7)": en évaluant par le parcours post-ordre de l'arbre d'origine, on retrouve . Toute divergence indique une inversion des opérandes dans la règle — l'erreur la plus fréquente sur cet exercice.
#Ce que ces sujets évaluent, en résumé
| Thème | Annale 2015-2016 | Session 2 2015-2016 | Session 2 2017-2018 |
|---|---|---|---|
| Analyse lexicale (lex) | 5 pts | 5 pts | — |
| Analyse sémantique et typage | 8 pts | 8 pts | — |
| Sethi-Ullman, code trois adresses | 7 pts | 7 pts | 7 pts |
| Traduction dirigée par la syntaxe | — | — | 8 pts |
| Expressions postfixées | — | — | 5 pts |
Trois mécanismes reviennent d'un sujet à l'autre et méritent d'être su par cœur: la règle du plus long match et l'ordre des règles dans lex; la distinction entre attributs synthétisés et hérités dans une traduction dirigée par la syntaxe; et la règle de décoration de Sethi-Ullman (, sinon ) avec la génération de code qui en découle. Le reste (structures de types, tables de symboles, actions yacc) se déduit de la grammaire, à condition de savoir lire une grammaire comme un constructeur de structures de données.