450 likes | 656 Views
Traduction dirigée par la syntaxe. Grammaires attribuées Attributs synthétisés Attributs hérités Arbres syntaxiques Grammaires S-attribuées Grammaires L-attribuées - schémas de traduction - traduction descendante - traduction ascendante. Objectifs.
E N D
Traduction dirigée par la syntaxe Grammaires attribuées Attributs synthétisés Attributs hérités Arbres syntaxiques Grammaires S-attribuées Grammaires L-attribuées - schémas de traduction - traduction descendante - traduction ascendante
Objectifs Faire la traduction pendant l'analyse syntaxique Ajouter des actions portant sur les attributs des symboles Obtenir un module qui fait à la fois l'analyse syntaxique et la traduction en une seule passe Analyse descendante Utiliser des fonctions qui ont des paramètres et renvoient des valeurs : les attributs Analyse ascendante La méthode est applicable avec Bison
Exemple Grammaire attribuée pour une calculette règle action L --> E '\n' print(E.val) E --> E + T E.val := E1.val + T.val E --> T E.val := T.val T --> T * F T.val := T1.val * F.val T --> F T.val := F.val F --> (E) F.val := E.val F --> chiffre F.val := chiffre.val
Numérotation des non-terminaux E --> E + T E.val := E1.val + T.val Si la règle comporte plusieurs fois un même non-terminal Une occurrence avec un indice dans l'action correspond à l'occurrence correspondante dans le membre droit de la règle Une occurrence sans indice correspond au membre gauche de la règle
Arbres décorés En ajoutant à un arbre de dérivation les attributs et leurs valeurs, on obtient un arbre décoré L E .val=19 \n E .val=15 T .val=4 + T .val=15 F .val=4 F .val=5 T .val=3 * F .val=3 nombre .val=5 nombre .val=4 nombre .val=3
Grammaires attribuées Une grammaire attribuée est définie par - une grammaire - des attributs associés à chaque symbole terminal ou non-terminal - une action associée à chaque règle X --> exprb := f(c1, c2, ... ck) Une action peut avoir des entrées c1, c2, ... ck et des sorties b qui sont des attributs de X et des symboles formant expr
Attributs synthétisés ou hérités X --> exprb := f(c1, c2, ... ck) L'attribut b est un attribut synthétisé si dans toutes les actions où il est calculé, c'est un attribut de X C'est un attribut hérité si dans toutes les actions où il est calculé, c'est un attribut d'un des symboles formant expr
Exemple d'attributs synthétisés Pour calculer les attributs synthétisés, on monte dans l'arbre L E .val=19 \n E .val=15 T .val=4 + T .val=15 F .val=4 F .val=5 T .val=3 * F .val=3 nombre .val=5 nombre .val=4 nombre .val=3
Exemple d'attributs hérités Déclaration de variables en C D --> T LL.type := T.type T --> int T.type := integer T -->float T.type := real L --> L , id L1.type := L.type ; ajouterType(id.entree, L.type) L -->id ajouterType(id.entree, L.type) L'attribut L.type est hérité
Construction d'un arbre Une grammaire attribuée qui construit un arbre représentant une expression arithmétique . . . . . . entrée pour c entrée pour a
Construction d'un arbre Fonctions utilisées makeNode(op, left, right) : crée un noeud dont l'étiquette est l'opérateur op et avec deux champs pour les pointeurs left et right makeLeaf(id, entree) : crée un noeud dont l'étiquette est id et avec un champ pour un pointeur vers une entrée de la table des symboles makeLeaf(num, val) : crée un noeud dont l'étiquette est num et avec un champ pour la valeur de la constante
Construction d'un arbre E --> E + T E.ptr := makeNode('+', E1.ptr, T.ptr) E --> E - T E.ptr := makeNode('-', E1.ptr, T.ptr) E --> T E.ptr := T.ptr T --> (E) T.ptr := E.ptr T --> id T.ptr := makeLeaf(id, id.entree) T --> num T.ptr := makeLeaf(num, num.val) Les deux attributs E.ptr et T.ptr contiennent des pointeurs sur des arbres construits
Grammaires S-attribuées Grammaires dont tous les attributs sont synthétisés Le calcul des attributs peut se faire dans la pile de l'analyseur ascendant
Exemple S --> E$ E --> E+T E --> T T --> T*F T --> F F --> (E) F --> N
Grammaires S-attribuées Calculer les attributs pendant les réductions A --> X Y Z A.a := f(X.x, Y.y, Z.z) Réduction : - calculer A.a en fonction des valeurs contenues dans la pile - dépiler X Y Z - empiler A - sauvegarder A.a dans la pile
Exemple L --> E '\n' print(val[top-1]) E --> E + T val[ntop] := val[top - 2] + val[top] E --> T /* inutile de recopier */ T --> T * F val[ntop] := val[top - 2] * val[top] T --> F F --> (E) val[ntop] := val[top - 1] F --> chiffre val[] : pile des valeurs d'attributs top : taille actuelle de la pile ntop : taille de la pile après la réduction en cours (se déduit de top et de la longueur de la règle)
Grammaires L-attribuées Grammaire dans laquelle le calcul des attributs peut être fait lors d'un parcours en profondeur de l'arbre de dérivation parcours(noeud n) { pourchaque fils m de n { calculer les attributs hérités de m ; parcours(m) ; } calculer les attributs synthétisés de n ; }
Définition formelle Une grammaire est L-attribuée si - tout attribut est synthétisé ou hérité ; - dans une règle A --> X1 X2 ...Xn, si un attribut Xi.b est calculé dans l'action associée, il ne dépend que des attributs des variables X1 X2 ...Xi-1 ou des attributs hérités de A
Exemple : le langage EQN EQN est un langage de composition de texte permettant d'écrire des formules mathématiques Chaque formule est contenue dans une boîte virtuelle Une boîte peut être en indice d'une autre Dans ce cas le corps de caractères (taille) de la boîte indice est plus petit De même pour une boîte en exposant La hauteur d'une boîte (B.ht) dépend - de la hauteur normale des caractères (texte.hn) - du corps de caractères (B.cc) - des indices ou exposants à l'intérieur
Exemple : le langage EQN S --> BB.cc := 10 ; S.ht := B.ht B --> B BB1.cc := B.cc ; B2.cc := B.cc ; B.ht := max(B1.ht, B2.ht) B --> B sub BB1.cc := B.cc ; B2.cc := diminue(B.cc) ; B.ht := position(B1.ht, B2.ht) B --> texteB.ht := texte.hn * B.cc diminue() applique un facteur d'échelle position() modifie la hauteur à cause de l'indice
Schémas de traduction 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 --> TR R --> addopT { print(addop.lexeme) } R | T --> num { print(num.val) }
Un schéma de traduction mal formé S --> AA { A1.val := 1 ; A2.val := 2 } A --> a { print(A.val) } La deuxième condition n'est pas satisfaite L'action qui calcule A1.val et A2.val est placée après
Le langage EQN S --> { B.cc := 10 } B { S.ht := B.ht } B --> { B1.cc := B.cc } B { B2.cc := B.cc } B { B.ht := max(B1.ht, B2.ht) } B --> { B1.cc := B.cc } B sub { B2.cc := diminue(B.cc) } B { B.ht := position(B1.ht, B2.ht) } B --> texte { B.ht := texte.hn * B.cc } Une grammaire L-attribuée peut toujours être mise sous la forme d'un schéma de traduction
Schémas de traduction en Bison Dans une spécification Bison, on peut insérer les actions dans le membre droit des règles Le traducteur engendré par Bison fera les actions au moment correspondant E : T R ; R :addop T { printf($1) ; } R | ; T : num{ print($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
Elimination 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'.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
Elimination de la récursivité à gauche E .he=9 E' E' .he=4 T .val=9 - T .val=5 T .val=2 + E' .he=6 nombre .val=9 nombre .val=5 nombre .val=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
Traduction descendante 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
Traduction ascendante 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)
Elimination des actions insérées On remplace le schéma de traduction par une grammaire L-attribuée en remplaçant certaines actions par de nouveaux non-terminaux appelés marqueurs Exemple E --> TR R --> +T { print('+') } R | -T { print('-') } R | T --> num { print(num.val) } devient : E --> TR R --> +TMR | -TNR | T --> num { print(num.val) } M --> {print('+') } N --> {print('-') }
Schémas de traduction en Bison Pour traiter un schéma de traduction Bison fait de même R :addop T { printf($1) ; } R | ; devient : R :addop T M R | ; M : { printf($1) ; } ;
Schémas de traduction en Bison La numérotation des symboles dans le membre droit des règles tient compte des actions Chaque action compte comme un symbole R :addop T { printf($1) ; } R | ; L'attribut de R est dans $4
Trouver un attribut dans la pile A --> X1 X2 ...Xn Quand on analyse Xi, les attributs hérités de A sont parfois calculables à partir d'autres attributs qui sont déjà dans la pile Exemple D --> T LL.type := T.type T --> int T.type := integer T -->float T.type := real L --> L , id L1.type := L.type ; ajouterType(id.entree, L.type) L -->id ajouterType(id.entree, L.type)
Trouver un attribut dans la pile L.type vient de T.type Le T en question est toujours juste au-dessous du L dans la pile
Trouver un attribut dans la pile On peut donc accéder à une case de la pile au-dessous de la règle en cours D --> T L /* inutile de recopier ici */ T --> intval[ntop] := integer T -->floatval[ntop] := real L --> L , idajouterType(val[top], val[top-3]) /* dans la pile : T L , id */ L -->id ajouterType(val[top], val[top-1]) /* dans la pile : T id */ L'action de la première règle est effectuée à la fin : trop tard pour recopier l'attribut
Trouver un attribut dans la pile Cette méthode est applicable avec Bison D : T L ; T : int { $$ := INTEGER ; } ; T : float { $$ := REAL ; } ; L : L , id { ajouterType($3, $0) ; /* dans la pile : T L , id */ } | id { ajouterType($1, $0) ; /* dans la pile : T id */ } ; Pour descendre dans la pile : $0, $-1, $-2...
Trouver un attribut dans la pile S --> a A C C.he := A.sy S --> b A B C C.he := A.sy C --> c C.sy := f(C.he) Quand on réduit c vers C, A.sy peut se trouver en val[top-1]ou en val[top-2] Il faut modifier la grammaire S --> a A C C.he := A.sy S --> b A B M C M.he := A.sy ; C.he := M.sy M --> M.sy := M.he C --> c C.sy := f(C.he) Quand on réduit c vers C, A.sy est toujours en val[top-1]
Trouver un attribut dans la pile S --> a A C C.he := g(A.sy) S --> b A B C C.he := A.sy C --> c C.sy := f(C.he) On modifie la grammaire S --> a A N C N.he := A.sy ; C.he := N.sy N --> N.sy := g(N.he) S --> b A B M C M.he := A.sy ; C.he := M.sy M --> M.sy := M.he C --> c C.sy := f(C.he) Quand on réduit c vers C, C.he est toujours en val[top-1]
Exemple : le langage EQN S --> L BB.cc := L.cc ; S.ht := B.ht L --> L.cc := 10 B --> B M BB1.cc := B.cc ; M.he := B.cc ; B2.cc := M.sy ; B.ht := max(B1.ht, B2.ht) .cc corps de caractères : 10 points, 12 points... .ht hauteur : distance entre la ligne de pied et le haut de la boîte .hn hauteur normale d'un caractère : hauteur du caractère lorsqu'il est composé en corps 1
Exemple : le langage EQN S --> L BB.cc := L.cc ; S.ht := B.ht L --> L.cc := 10 B --> B M BB1.cc := B.cc ; M.he := B.cc ; B2.cc := M.sy ; B.ht := max(B1.ht, B2.ht) M --> M.sy := M.he B --> B sub N BB1.cc := B.cc ; N.he := B.cc ; B2.cc := N.sy ; B.ht := position(B1.ht, B2.ht) N --> N.sy := diminue(N.he) B --> texteB.ht := texte.hn * B.cc
Exemple : le langage EQN S --> L B val[ntop] := val[top] L --> val[ntop] := 10 B --> B M B val[ntop] := max(val[top-2], val[top]) /* dans la pile : L B M B */ M --> val[ntop] := val[top-1] /* dans la pile : L B */ B --> B sub N B val[ntop] := position(val[top-3], val[top]) N --> val[ntop] := diminue(val[top-2]) /* dans la pile : L B sub */ B --> texte val[ntop] := val[top] * val[top-1] /* dans la pile : L B */
Algorithme général Donnée : une grammaire L-attribuée Résultat : un traducteur ascendant On suppose que chaque non-terminal A a un attribut hérité A.he et que chaque symbole X a un attribut synthétisé X.sy Remplacer chaque règle A --> X1 X2 ...Xn par A --> M1 X1 M2 X2 ... Mn Xn Associer les Xi.he aux Mi Quand on réduit vers Mi, la position de A.he, X1.he X2.he ... dans la pile se déduit de la nouvelle grammaire
Résumé Les schémas de traduction permettent d'incorporer la traduction à l'analyse syntaxique pour obtenir un traducteur en une passe Les attributs synthétisés sont faciles à calculer - analyse descendante : valeurs des fonctions associées aux non-terminaux de la grammaire - analyse ascendante : dans la pile Les attributs hérités sont calculés - en analyse descendante : comme paramètres des fonctions - en analyse ascendante : en remontant dans la pile, et s'il le faut en introduisant des non-terminaux "marqueurs"