Analyse lexicale et syntaxique Chapitre 4 Analyse lexicale et syntaxique
Chapitre 4: Sujets Introduction Analyse lexicale Analyse syntaxique Méthode descendante Méthode ascendante Copyright © 2007 Addison-Wesley. All rights reserved.
Introduction 3 principales méthodes pour implémenter un langage de programmation: compilation interprétation méthode hybride Dans tous les cas, il est nécessaire de se référer au code source pour détecter les erreurs de syntaxe. Pratiquement toutes les méthodes d'analyse de la syntaxe reposent sur une description formelle de la syntaxe du langage (grammaire hors-contexte) Copyright © 2007 Addison-Wesley. All rights reserved.
Analyse syntaxique Deux parties: Au niveau inférieur il y a l'analyseur lexical (automates finis) Au niveau supérieur il y a l'analyseur syntaxique ou parseur (automates à pile) Copyright © 2007 Addison-Wesley. All rights reserved.
Décrire la syntaxe à l'aide de grammaires Procure une description claire et concise Le parseur peut être construit à partir de la grammaire La séparation des analyses lexicale et syntaxique facilite la maintenance car modulaire Copyright © 2007 Addison-Wesley. All rights reserved.
Avantages de la séparation Simplicité – méthodes moins complexes peuvent être utilisées pour l'analyse lexicale; simplifie le parseur Efficacité- séparation permet l'optimisation de l'analyseur lexical. Portabilité – l'analyseur lexical dépend souvent de la plateforme (lecture de fichiers) alors que l'analyseur syntaxique est toujours portable. Copyright © 2007 Addison-Wesley. All rights reserved.
Analyse lexicale Agit comme interface pour l'analyseur syntaxique Identifie les parties du programme source correspondant aux lexèmes Un lexème est un mot correspondant à une catégorie de mots (token) Par exemple, la variable somme est un lexème appartenant au token identificateur Copyright © 2007 Addison-Wesley. All rights reserved.
Analyse lexicale (suite) L'analyseur syntaxique appelle l'analyseur lexical lorsqu'il a besoin d'un autre token 3 approches pour construire un analyseur lexical: Écrire une description formelle des tokens (expressions régulière) et utiliser un outil logiciel (ex. lex) pour transformer ces descriptions en un analyseur lexical. Concevoir un diagramme d'état pour décrire les tokens et implémenter ce diagramme sous forme de programme. Concevoir un diagramme d'état pour décrire les tokens et implémenter ce diagramme à l'aide d'une table. Copyright © 2007 Addison-Wesley. All rights reserved.
Construire un analyseur lexical Par exemple, si on veut lire trois types de tokens: identificateurs: lettre suivie de lettres ou de chiffres. mots clefs: suite de lettres entiers littéraux: chiffre suivit de chiffres Approche naïve: Une transition pour chaque état et chaque caractère – trop gros! Copyright © 2007 Addison-Wesley. All rights reserved.
Construire un analyseur lexical (suite) Plusieurs transition peuvent être combinées pour simplifier le diagramme Une seule classe de caractères pour toutes les lettres majuscules et minuscule (LETTER) Une seule classe de caractères pour tous les chiffres (DIGIT) Copyright © 2007 Addison-Wesley. All rights reserved.
Construire un analyseur lexical (suite) Plutôt que d'avoir des états distincts pour les mots réservés, on traite ceux-ci de la même manière que les identificateurs On consulte une table pour déterminer si un lexème est un mot clef Copyright © 2007 Addison-Wesley. All rights reserved.
Diagramme d'état Copyright © 2007 Addison-Wesley. All rights reserved.
Construire un analyseur lexical (suite) Variables globales et utilitaires: char nextChar: contient le dernier caractère lu int charClass: LETTER (0), DIGIT (1), UNKNOWN (-1) char lexeme[100]: chaîne de caractère getChar() – lit le prochain caractère et le place dans nextChar. Met la classe du caractère dans charClass. addChar() – Ajoute nextChar à la fin de lexeme int lookup(char*) – détermine si lexeme est un mot clef Copyright © 2007 Addison-Wesley. All rights reserved.
Implémentation int lex() { getChar(); switch (charClass) { case LETTER: addChar(); while (charClass == LETTER || charClass == DIGIT) { } return lookup(lexeme); break; … Copyright © 2007 Addison-Wesley. All rights reserved.
Implémentation … case DIGIT: addChar(); getChar(); while (charClass == DIGIT) { } return INT_LIT; break; } /* End of switch */ } /* End of function lex */ Copyright © 2007 Addison-Wesley. All rights reserved.
L'analyseur syntaxique Objectifs: Trouver toutes les erreurs de syntaxe et envoyer un message approprié. Produire l'arbre syntaxique ou, du moins, l'information nécessaire pour le construire. Copyright © 2007 Addison-Wesley. All rights reserved.
L'analyseur syntaxique (suite) Deux catégories d'analyseurs syntaxiques: Descendant – produit l'arbre syntaxique en commençant par la racine Ascendant – commence par les feuilles Dans les deux cas on utilise une grammaire hors-contexte comme description du langage. Copyright © 2007 Addison-Wesley. All rights reserved.
L'analyseur syntaxique (suite) Complexité À partir de toute grammaire non ambiguë, il est possible de construire un analyseur syntaxique fonctionnant en temps O(n3), où n est la taille de l'entrée. On utilise plutôt un type restreint de grammaire permettant d'effectuer l'analyse syntaxique en temps O(n) Copyright © 2007 Addison-Wesley. All rights reserved.
L'analyseur syntaxique (suite) Parseurs descendants On commence avec le symbole de départ et on détermine la séquence de règles nécessaires pour dériver l'entrée (suite de token) <départ> entrée Algorithme LL: Left-to-right scan (lecture de l'entrée de gauche à droite) Leftmost derivation (dérivation par la gauche) Copyright © 2007 Addison-Wesley. All rights reserved.
L'analyseur syntaxique (suite) Parseurs ascendants On commence avec l'entrée et on applique à rebours les règles de la grammaire afin d'obtenir la symbole de départ. entrée <départ> Algorithme LR: Left-to-right scan Rightmost derivation Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseur récursif-descendant (LL) Un sous-programme pour chaque non-terminal Les sous-programmes sont mutuellement récursifs Les grammaires hors-contextes étendues sont appropriées pour ce type d'analyseur syntaxique car elles minimisent le nombre de sous- programmes. On suppose l'existence d'un analyseur lexical lex, qui met le prochain token dans la variable globale nextToken Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseur récursif-descendant (suite) Exemple: <expr> <term> {(+ | -) <term>} <term> <factor> {(* | /) <factor>} <factor> id | ( <expr> ) Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseur récursif-descendant (suite) // // Sous-programme pour le non-terminal expr void expr() { term(); // Sous-programme pour le non-terminal term while (nextToken == PLUS_CODE || nextToken == MINUS_CODE){ lex(); term(); } } Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseur récursif-descendant (suite) // // Sous-programme pour le non-terminal term void term() { factor(); // Sous-programme pour le non-terminal factor while (nextToken == MULT_CODE || nextToken == DIV_CODE){ lex(); factor(); } } Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseur récursif-descendant (suite) void factor() { if (nextToken) == ID_CODE) lex(); else if (nextToken == LEFT_PAREN_CODE) { expr(); if (nextToken == RIGHT_PAREN_CODE) else error(); } else error(); Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseur récursif-descendant (suite) Grammaires LL Forme particulière Le problème de la récursion à gauche: A → Ab ou A → Bb B → Ac De telles grammaires ne peuvent pas être utilisée par un analyseur syntaxique descendant On peut cependant toujours remplacer une telle grammaire par une autre n'ayant pas ce problème Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseur récursif-descendant (suite) Autre problème: Deux règles: A→α et A→β tels que α génère aα' et β génère aβ' Comment choisir entre ces deux règles? La grammaire doit être modifiée mais cela n'est pas toujours possible Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseurs ascendants Forme sententielle droite: On développe le non- terminal le plus à droite en premier Exemple: E → E + T | T T → T * F | F F → (E) | id E ⇒ E+T ⇒ E+T*F ⇒ E+T*id ⇒ E+F*id ⇒ E+id*id ⇒ F+id*id ⇒ id+id*id On veut partir de id+id*id et remonter vers E Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseurs ascendants (suite) Handle: segment d'une forme sententielle (dérivée par la droite) correspondant à la partie à droite de la dernière règle utilisée Exemple: E ⇒ E+T ⇒ E+F ⇒ E+id ⇒ E+T+id E+T est le handle de E+T+id Remarque: Si la grammaire est non-ambiguë alors le handle est unique Problème: Comment trouver le handle? Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseurs ascendants (suite) Les algorithmes Shift-Reduce: Utilisent une pile Déplacement (Shift): Action de placer le prochain token sur le dessus de la pile Reduction: Action de remplacer le handle sur le dessus de la pile par la partie de gauche (non- terminal) de la règle correspondante Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseurs ascendants (suite) Parseurs LR Type particulier d'analyseurs ascendants conçus par D. Knuth en 1965 Ils fonctionnent pour pratiquement toutes les grammaires décrivant des langages de programmation Ils peuvent détecter les erreurs de syntaxe aussitôt que possible. Plus généraux que les parseurs LL Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseurs ascendants (suite) Un parseur LR est un automate fini augmenté d'une pile Une configuration de la machine a la forme: (S0 X1 S1 X2 S2 … Xm Sm, aiai+1…an$) aiai+1…an : Partie de l'entrée qui reste à lire Si : États de la machine Xi : Symboles de la grammaire Copyright © 2007 Addison-Wesley. All rights reserved.
Structure d'un parseur LR Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseurs ascendants (suite) Le comportement d'un parseur LR est indiqué par deux tables: la table ACTION (lignes=états, colonnes=terminaux) la table GOTO (lignes=états, colonnes=non-terminaux) La table ACTION indique si un déplacement ou une réduction doit avoir lieu étant donné l'état courant et le prochain token (terminal) La table GOTO indique quel état placer sur le dessus de la pile après qu'une réduction ait eu lieu. Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseurs ascendants (suite) Configuration initiale: (S0, a1…an$) Configuration courante: (S0X1S1X2S2…XmSm, aiai+1…an$) Comportement du parseur: Si ACTION[Sm, ai] = Shift S, la prochaine configuration sera: (S0X1S1X2S2…XmSmaiS, ai+1…an$) Si ACTION[Sm, ai] = Reduce A et que S = GOTO[Sm-r, A], où r = ||, alors la prochaine configuration sera: (S0X1S1X2S2…Xm-rSm-rAS, aiai+1…an$) Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseurs ascendants (suite) Comportement du parseur (suite): Si ACTION[Sm, ai] = Accept, alors l'analyse est complété et aucune erreur n'a été trouvée. Si ACTION[Sm, ai] = Error, alors le parseur appelle un utilitaire de gestion des erreurs. Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseurs ascendants (suite) Exemple: E → E + T E → T T → T * F T → F F → ( E ) F → id Les tables ACTION et GOTO sont habituellement construite à l'aide d'un programme telle que yacc ou bison Copyright © 2007 Addison-Wesley. All rights reserved.
Les tables ACTION et GOTO Copyright © 2007 Addison-Wesley. All rights reserved.
Analyseurs ascendants (suite) Pile Entrée Action id + id * id $ Shift 5 0 id 5 + id * id $ Reduce 6 (GOTO[0,F]) 0 F 3 Reduce 4 (GOTO[0,T]) 0 T 2 Reduce 2 (GOTO[0,E]) 0 E 1 Shift 6 0 E 1 + 6 id * id $ 0 E 1 + 6 id 5 * id $ Reduce 6 (GOTO[6,F]) 0 E 1 + 6 F 3 Reduce 4 (GOTO[6,T]) 0 E 1 + 6 T 9 Shift 7 0 E 1 + 6 T 9 * 7 id $ 0 E 1 + 6 T 9 * 7 id 5 $ Reduce 6 (GOTO[7,F]) 0 E 1 + 6 T 9 * 7 F 10 Reduce 3 (GOTO[6,T]) Reduce 1 (GOTO[0,E]) Accept E → E + T E → T T → T * F T → F F → ( E ) F → id Copyright © 2007 Addison-Wesley. All rights reserved.
Comprendre les tables: les items (1) T → 0T0 (2) T → 1 Items: S → _T$ T → _0T0 T → _1 S → T_$ T → 0_T0 T → 1_ S → T$_ T → 0T_0 T → 0T0_ Items complets: S → T$_ T → 1_
Signification des items S→_T$ Un item de la forme A→α_β indique que l'on est en train de traiter la règle A→αβ alors que α est sur les dessus de la pile Un item complet de la forme A→γ_ indique que γ est sur le dessus de la pile et qu'on peut le remplacer par A (réduction) T S→T_$ $ S→T$_ T→0_T0 T T→0T_0 T T→0T0_ T→0_T0 T T→0T_0 T→0T0_ T 1 T→1_
Signification des items (suite) S→_T$ Analyse du mot 00100$ Shifts (3 fois) Reduce Shift Reduce 1, T→1_ 0, T→0_T0 S, S→_T$ T, T→0T_0 0, T→0_T0 S, S→_T$ 0, T→0T0_ T, T→0T_0 0, T→0_T0 S, S→_T$ T, T→0T_0 0, T→0_T0 S, S→_T$ T S→T_$ $ S→T$_ T→0_T0 T T→0T_0 T T→0T0_ Shift Reduce Shift Reduce T→0_T0 T T→0T_0 T→0T0_ T 0, T→0T0_ T, T→0T_0 0, T→0_T0 S, S→_T$ T, S→T_$ S, S→_T$ $, S→T$_ T, S→T_$ S, S→_T$ S, accept S, S→_T$ 1 T→1_
Automate fini S→_T$ 2 T→0_T0 4 T→0T_0 5 T→0T0_ T T 1 1 Cet automate sert à décrire toutes les formes sententielles ne contenant pas de handle. Chaque état correspond à un item Seules les formes sententielles conduisant à un item complet possèdent un handle. 1 S→T_$ 3 T→1_ $ <accept> S→T$_ 0) S → T$ 1) T → 0T0 2) T → 1
Construction des tables à partir de l'automate ACTION GOTO 1 $ T S2 S3 accept 2 4 3 R2 S5 5 R1
Autre exemple Ajoutons 3 règles à la grammaire précédente: (0) S → T$ (1) T → 0T0 (2) T → 1 (3) T → 0V (4) V → 2V (5) V →2 Note: Ceci n'est pas une grammaire LL mais c'est une grammaire LR
Automate fini non-déterministe S→_T$ 2 T→0_T0 4 T→0T_0 5 T→0T0_ T T 1 1 1 S→T_$ 3 T→1_ 7 V→2_ 2 $ 2 9 V→2V_ 6 T→0_V 8 V→2_V <accept> S→T$_ V 2 10 T→0V_ V 2
Automate fini déterministe 4 T→0T_0 5 T→0T0_ S→_T$ 2 T→0_T0 T 1 On peut toujours transformer un automate fini non-déterministe en un automate fini déterministe équivalent. Chaque état correspond à un ensemble d'items. 1 1 S→T_$ 3 T→1_ T $ 2,6 T→0_T0 T→0_V <accept> S→T$_ 9 V→2V_ 7,8 V→2_ V→2_V V 10 T→0V_ 2 V 2
Les conflits Il existe deux types de conflits possibles lors de la construction des tables à partir de l'automate: Shift-Reduce Reduce-Reduce
Conflits Shift-Reduce Configuration initiale: (S0, 000$) Shift: (S0 0 S1, 00$) Shift ou Reduce ? Bison choisit toujours le Shift dans ce cas. Remarque: Le mot ne sera donc pas accepté alors qu'il devrait l'être.
Conflits Reduce-Reduce S → 0 S 0 S → T T → S 0 T → 1 Configuration initiale: (S0, 010$) Shift: (S0 0 S1, 10$) Shift: (S0 0 S1 1 S2, 0$) Reduce: (S0 0 S1 T S3, 0$) Reduce: (S0 0 S1 S S3, 0$) Shift: (S0 0 S1 S S3 0 S4, $) Réduce: on utilise la règle 1 ou 3 ? Yacc choisit toujours la première des règles dans ce cas. Remarque: La grammaire n'est pas ambiguë.
Résumé L'analyse syntaxique est une partie essentielle de L'implémentation d'un langage Un analyseur lexical traduit une séquence de caractère (le programme source) en une séquence de tokens (et de lexèmes) Un parseur récursif-descendant est un parseur LL implémenté en écrivant le code directement à partir de la grammaire du langage Un parseur LR est l'approche la plus commune pour implémenter les analyseurs syntaxiques ascendants; c'est approche utilisé par Yacc. Copyright © 2007 Addison-Wesley. All rights reserved.