• Aucun résultat trouvé

IFT313 Introduction aux langages formels

N/A
N/A
Protected

Academic year: 2022

Partager "IFT313 Introduction aux langages formels"

Copied!
23
0
0

Texte intégral

(1)

IFT313

Introduction aux langages formels

Froduald Kabanza

Département d’informatique Université de Sherbrooke

Analyseurs récursifs LL (1)

(2)

Sujets

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

• Comment le programmer ?

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

syntaxique récursif ?

(3)

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.

(4)

© Froduald Kabanza

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

(5)

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.

(6)

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’ TFT’

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

F F n F(E)

( )

E  TE’

(7)

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’ TFT’

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

F F n F(E)

( )

E  TE’

(8)

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.

(9)

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)

(10)

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(else); S(); 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é.

(11)

Analyseur LL(1) récursif (suite)

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”);

} 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

}

(12)

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.

(13)

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.

(14)

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.

(15)

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 ?

(16)

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 }

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 terminal, 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’in- struction switch.

De cette façon, on obtient un parser LL(1) récursif, équiva- lent 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 ?

(17)

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.

(18)

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’ TFT’

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 E’() { switch (a)

case + : match(+);

T();E’(); break;

case ) : break;

case

EOF

: break;

default : error()}

(19)

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.

(20)

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 E’() {

switch (a)

case + : match(+);

T();E’(); break;

case ) : break;

case EOF : break;

default :

print(“Expected +, ), or

EOF.”);

}

(21)

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 E’() {

switch (a)

case + : match(+);

T();E’(); break;

case ) : break;

case EOF : break;

default :

print(“Expected +, ), or EOF.”);

}

(22)

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 E’() {

switch (a)

case + : match(+); T();E’(); break;

case ) : break;

case EOF : break;

default :

print(“Expected +, ), or EOF.”);

skipTo(Follow[E’]);}

Follow[Eprime] = { ), $ }

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

(23)

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.

Références

Documents relatifs

• Deux états dans d’un AFD M sont équivalents (indistinguable) si et seulement si pour chaque mot u, lorsque une exécution de M sur l’entrée u commence dans l’un ou

- Pour analyser la syntaxe d’une grammaire hors-contexte, nous ajoutons une pile à un automate fini pour obtenir un modèle de programmes plus puissant connu sous le nom de

 Pour ce faire la partie gauche de la production (un non-terminal) est enlevée de la pile et remplacée par la partie droite de la production. (q, ε, A)  (q, a) pour chaque

- On vient de voir qu’en calculant First(S) ou First(XYS) on doit tenir compte des non-terminaux qui pourraient dériver la chaîne vide et de ceux qui pourraient les suivre dans

– Si le symbole est un non terminal, l’attribut pourrait être une donnée calculée en utilisant les actions sémantiques. – Une grammaire avec des attributs est appelée

• Pour aller au-delà, il faut utiliser d’autres outils ou des méthodes adhoc pour évaluer les attributs (avec l’aide

• 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

AFD pour préfixes viables, avec état initial I0 et fonction de transition goto Initialement la pile contient I0.. L’état I2 contient élément Shift (T ® T.*F) et un élément