IFT313 Introduction aux langages formels Froduald Kabanza Département dinformatique Université de...

Post on 04-Apr-2015

105 views 0 download

Transcript of IFT313 Introduction aux langages formels Froduald Kabanza Département dinformatique Université de...

IFT313 Introduction aux langages formels

Froduald Kabanza

Département d’informatique

Université de Sherbrooke

Analyseurs récursifs LL (1)

Sujets

• C’est quoi un analyseur syntaxique récursif ?

• Comment le programmer ?

• Comment fonctionne un générateur d’analyseur

syntaxique récursif ?

IFT313 2© Froduald Kabanza

Objectifs

• Pouvoir programmer un analyseur syntaxique récursif pour une grammaire donnée.

• Connaître les fondements d’un générateur d’analyseur

syntaxique LL tel que JavaCC.

IFT313 3© Froduald Kabanza

© Froduald Kabanza

4IFT313

Références

[2] Appel, A. and Palsberg. J. Modern Compiler Implementation in Java.

Second Edition. Cambridge, 2004.– Section 3.2

[4] Aho, A., Lam, M., Sethi R., Ullman J. Compilers: Principles, Techniques, and

Tools, 2nd Edition. Addison Wesley, 2007.– Section 4.4.1

Rappel : Analyseur LL(1) non récursif- Un analyseur syntaxique LL non récursif exécute une boucle dans laquelle,

à chaque étape, soit il prédit la production à appliquer ou il reconnaît (match) le prochain lexème (token).

Pour cette raison, on l’appelle souvent en anglais « predictive parser »

ou « predict-match parser ».

- Un générateur d’analyseur syntaxique non récursif :• Prend une grammaire comme entrée.• Produit, à partir de la grammaire, une table d’analyse qui prédit la

production à appliquer en fonction du non terminal au sommet de la pile et du prochain lexème (token).

• Le générateur a accès à du code pour un driver LL(1) (qui est essentiellement un automate à pile LL(1))

• L’analyseur pour la grammaire d’entrée est obtenu en combinant le driver et la table d’analyse.

IFT313 5© Froduald Kabanza

Rappel : Exemple

G = (V, A, R, E) :

V = {E, E’, T, T’, F}

A = {(, ), +, *, n}

R = {

E TE’

E’ + TE’ | ε

T FT’ T’ *FT’ | ε F ( ) | E

n }

Table d’analyse

n + *

E E TE’

$

E’ E’+TE’ E’ ε E’ εT T FT’ TFT’

T’ T’ ε T’*FT’ T’ ε T’ εF F n F(E)

( )

E TE’

IFT313 6© Froduald Kabanza

return true

Pile

0.3.3.3.2.3.3.2.3.3.2.3.2.3.2.3.3.1.

Étape Règle Algorithm LLDriver

0. stack = ($S); a = in.read(); x=stack.top();

while (true) {

1. if (x = = $) && (a= = $)

return true ;

2. if (x = = a) && (a != $) {

pop a from stack; a = in.read();

continue;}

3. if x is a nonterminal {

if M[x,a] is error exit with error;

let x y in M[x,a]

pop x from stack; push y on stack;

continue; }

4. exit with error;}

Entrée

Entrée : n+n*n

$E$E’T$E’T’F$E’T’n$E’T’$E’$E’ T+$E’ T$E’T’F$E’T’n$E’T’$E’T’F*$E’T’F$E’T’n$E’T’$E’$

n+n*n$ n+n*n$ n+n*n$n+n*n$ +n*n$ +n*n$ +n*n$ n*n$ n*n$ n*n$ *n$ *n$ n$ n$ $ $ $

E TE’T FT’F n

T’ εE’ +TE’

T FT’F n

T’ *FT’

F n T’ εE’ ε

n + *

E E TE’

$

E’ E’+TE’ E’ ε E’ ε

T T FT’ TFT’

T’ T’ ε T’*FT’ T’ ε T’ ε

F F n F(E)

( )

E TE’

IFT313 7© Froduald Kabanza

Analyse LL(1) descendante récursive - On peut aussi définir un analyseur LL(1) directement à partir des règles de

productions et de la table d’analyse, sans utiliser le driver LL1.

- L’idée est de simuler directement la dérivation la plus à gauche : En associant des fonctions d’analyse aux différents symboles de la

grammaire (terminaux et non terminaux). En faisant les appels de fonctions selon la structure de la grammaire.

- Aux terminaux on associe une fonction match(Token) qui va matcher le prochain token.

- A chaque non terminal X, on associe une fonction X() dont le corps appelle des fonctions correspondant aux parties droites des règles dont X est la partie gauche.

IFT313 8© Froduald Kabanza

Exemple

G = (V, A, R, S) :

V = {S, T, L, E}

A ={if, else, {, }, ;, =, ), (, id, print}

R = {

S if T S else S

S { S L | print(E)

T (id = = id)

L } | ;S L

E id }

Exemple de mot généré : if (id = = id) { print(id); print(id) } else print(id)

IFT313 9© Froduald Kabanza

Analyseur LL(1) récursif

G = (V, A, R, E) :

V = {S, T, L, E} A ={ if, else, {, }, ;, =, ),

(, id, print }

R = {

S if T S else S

S { S L | print(E)

T (id = = id)

L } | ;S L

E id }

Token a; // Variable globale : contiendra le prochain token

void match (GrammarSymbol x){ if (x.equals(a.text()) a = getNextToken(); else error();}

void S() { switch (a) case if : match(if); T(); S(); match(‘;’); match(else); S(); match(‘;’); break; case ‘{’ : match(‘{’); S(); L(); break; case print : match(print); match(‘(’); E(); match(‘)’); break; default: error();}

void T() { switch (a) case ‘(’ : match(‘(’); match(id); match(=); match(=); match(id); match(‘)’); break; default : error();}

Note : En pratique ‘;’ sera représenté par un symbole (ex. SEMI). Idem pour {, }, (, ).Ce n’est pas fait ici pour une question de clarté

IFT313 10© Froduald Kabanza

Analyseur LL(1) récursif (suite)

G = (V, A, R, S) :

V = {S, T, L, E}

A ={ if, else, {, }, ;, =, ),

(, id, print }

R = {

S if T S else S

S { S L | print(E)

T (id = = id)

L } | ;S L

E id }

void L() { switch (a) case ‘}’ : match(‘}’); break; case ‘;’ : match(‘;’); S(); L(); break; default : error();}

void E() { switch (a) case id : match(id); break; default : error();}

void main () // Point d’entrée du parseur { a = getNextToken(); S(); // fonction d’analyse pour le symbole de départ System.out.print(“Accepte : entrée correcte”);}

IFT313 11© Froduald Kabanza

Exercices

- Pour vous convaincre que ça marche, simulez l’analyseur sur les entrées suivantes :

Entrée incorrecte syntaxiquement :

if else (id = = id)

Entrée correcte syntaxiquement :

if (id = = id) { print(id); print(id) } else print(id);

- Modifiez le parseur pour qu’il imprime la dérivation de l’entrée.- Implémentez-le en Java.

IFT313 12© Froduald Kabanza

Observations- Il est facile d’écrire un analyseur syntaxique récursif manuellement.

- Pour que l’approche précédente fonctionne il faut que :

1. La partie droite de chaque production commence par un terminal

Parce que le switch de chaque fonction X() se fait sur les terminaux qui commencent les partie droite des production dont X est la partie gauche.

2. Deux productions ayant la même partie gauche doivent avoir des parties droites commençant par des préfixes différents.

Parce que les deux règles ont la même fonction d’analyse (c-à-d., la fonction correspondant au non terminal dans la partie gauche de chaque production). Si elle partagent le même préfixe, le switch ne pourra pas tenir compte des deux à la fois.

IFT313 13© Froduald Kabanza

Observations- Il est facile d’écrire un analyseur syntaxique récursif manuellement.

- Pour que l’approche précédente fonctionne il faut que :

1. La partie droite de chaque production commence par un terminal.

2. Deux productions ayant la même partie gauche doivent avoir des parties droites commençant par des préfixes différents.

- Ces conditions nous garantissent que la fonction d’analyse pour chaque non terminal est déterministe.

En d’autre mots, on peut prédire la production appropriée, simplement en lisant le prochain token.

IFT313 14© Froduald Kabanza

Observations- Il est très facile d’écrire un analyseur syntaxique récursif manuellement. - Pour que l’approche précédente fonctionne il faut que :

1. La partie droite de chaque production commence par un terminal

2. Deux productions ayant la même partie gauche doivent avoir des parties droites commençant par des préfixes différents.

- Ces conditions nous garantissent que la fonction d’analyse pour chaque non terminal est déterministe.

- Nous avons vu que seulement la première condition n’est pas nécessairement requise pour un parseur LL(1) non récursif.

- Comment généraliser l’approche récursive pour que la condition 1 ne soit pas nécessaire ?

IFT313 15© Froduald Kabanza

ExempleG = (V, A, R, E) :

V = {E, E’, T, T’, F}

A = {(, ), +, *, n}

R = {

E TE’

E’ + TE’ | ε

T FT’ T’ *FT’ | ε F ( ) | E n }

Avec l’approche précédente on s’attendrait à quelque chose

du genre :

void E()

{ switch (a)

case ?? : T(); Eprime(); break;

default : error()}

Mais qu’est-ce qu’on met aux endroits indiqués par « ?? » ?

Vu que la production E TE’ ne commence pas par un ter-minal, notre approche ne fonctionne plus.

Pour résoudre ce problème, il faut utiliser la table d’analyse LL(1) de la grammaire, pour implémenter les cas de l’ins-truction switch.

De cette façon, on obtient un parser LL(1) récursif, équi-valent au parser LL(1) non récursif.

Cette grammaire illustre les limites de l’approche précédente.

Par exemple, quelle est la fonction d’analyse pour le non-terminal E ?

IFT313 16© Froduald Kabanza

Analyse syntaxique LL(1) récursif

- En général, pour avoir un analyseur syntaxique récursif, il faut utiliser une table d’analyse LL(1) afin d’implémenter les cas du switch:

Pour une fonction d’analyse X() donnée, les cas de l’instruction switch correspondent aux tokens a, tels que les entrées [X,a] sont non vides dans la table d’analyse.

La séquence d’appels pour chaque chaque cas est une séquence de match et de fonction d’analyse correspondants à la partie droite de la production dans l’entrée [X,a] de la table d’analyse.

IFT313 17© Froduald Kabanza

Exemple

G = (V, A, R, E) :

V = {E, E’, T, T’, F}

A = {(, ), +, *, n}

R = {

E TE’

E’ + TE’ | ε

T FT’ T’ *FT’ | ε F ( ) | E

n }

Table d’analyse

n + *

E E TE’

$

E’ E’+TE’ E’ ε E’ εT T FT’ TFT’

T’ T’ ε T’*FT’ T’ ε T’ εF F n F(E)

( )

E TE’

void E() { switch (a) case n : T(); Eprime(); break; case ( : T(); Eprime(); break; default : error()}

void Eprime() { switch (a) case + : match(+); T();E’(); break; case ) : break; case EOF : break; default : error()}

IFT313 18© Froduald Kabanza

Stratégies de recouvrement d’erreurs

- Une erreur apparaît lorsque la chaîne d’entrée n’est pas syntaxiquement correcte, c-à-d. elle n’est pas dérivable de la grammaire.

- En pratique, on ne veut pas arrêter l’analyse à la toute première erreur.

On veut continuer l’analyse syntaxique jusqu’à un certain nombre d’erreurs préfixé ou jusqu’à un certain niveau de sévérité de l’erreur.

- Les stratégies de recouvrement typiques consistent à réparer la chaîne d’entrée pour que l’analyse continue. En particulier :

- On peut insérer des tokens.- Supprimer des tokens.- Remplacer des tokens.

IFT313 19© Froduald Kabanza

Recouvrement d’erreurs par insertion de tokens

- Pour insérer un token manquant de l’input, on n’a pas besoin de l’ajouter explicitement à la chaîne d’entrée.

- Il suffit de prétendre que le token est présent, imprimer un message approprié et retourner normalement tel qu’illustré par les exemples suivants pour E() et Eprime().

void E() { switch (a) case n : T(); Eprime(); break; case ( : T(); Eprime(); break; default : print(“Expected num or )”;}

void Eprime() { switch (a) case + : match(+); T();E’(); break; case ) : break; case EOF : break; default : print(“Expected +, ), or EOF.”); }

IFT313 20© Froduald Kabanza

Recouvrement d’erreurs par insertion de tokens

- Le recouvrement d’erreurs par insertion de tokens est à utiliser avec précaution parce que une cascade d’erreurs risque de mener à une à une boucle sans fin : tokens sont insérés (ou supposés présents) sans cesse, de sorte que la chaine d’entrée n’est jamais vidée.

void E() { switch (a) case n : T(); Eprime(); break; case ( : T(); Eprime(); break; default : print(“Expected num or )”;}

void Eprime() { switch (a) case + : match(+); T();E’(); break; case ) : break; case EOF : break; default : print(“Expected +, ), or EOF.”); }

IFT313 21© Froduald Kabanza

Recouvrement d’erreurs par suppression de tokens

- Le recouvrement d’erreurs par suppression de tokens est plus sécuritaire parce qu’il garantie toujours que la chaîne d’entrée va être vidée.

- Pour une fonction d’analyse X(), la stratégie est, en cas d’erreur, de sauter (supprimer) les prochains tokens jusqu’au premier token qui est dans Follow(X).

void Eprime() { switch (a) case + : match(+); T();E’(); break; case ) : break; case EOF : break; default : print(“Expected +, ), or EOF.”); skipTo(Follow[Eprime]);}

Follow[Eprime] = { ), $ }

skipTo(A) supprime les prochains tokens jusqu’au premier dans A.

IFT313 22© Froduald Kabanza

Générateurs d’analyseurs LL(1) récursifs

- Un générateur d’analyseur LL(1) récursif reçoit comme entrée une grammaire et donne comme sortie un analyseur LL(1) récursif correspondant.

- Pour ce faire : Il génère une table d’analyse LL(1) Génère un patron (template) des fonctions d’analyse à partir des règles

de production, utilisant la table d’analyse pour implémenter le switch. Ajoute le code pour la méthode (fonction) match.

- Il n’y a plus de pile explicite. Elle est implicitement implémentée par la pile d’appels des fonctions (la pile de récursivité).

- JavaCC et ANTLR sont des exemple de générateurs d’analyseurs LL récursifs.

IFT313 23© Froduald Kabanza