La syntaxe - Département Montefiore
Transcription
La syntaxe - Département Montefiore
INFO0004 – Structure des langages de programmation Analyse lexicale et syntaxique Justus H. Piater Résumé Résumé – Analyse fichier tex te Program m ation Analyse lex icale Analyse flux de jetons Com pilation Analyse syntax ique Liaison Interprétation Ex écution arbre de syntax e abstraite Analyse sém antique arbre annoté Résumé 3 / 58 Syntaxe et sémantique Un langage de programmation doit être précis, sans ambiguïté. La syntaxe : La sémantique : Résumé spécifie la structure d’un programme valide (p.ex., par une grammaire) spécifie la signification d’un programme valide. 4 / 58 Un programme quelconque trosq [] = [] trosq (x:xs) = trosq elts_lt_x ++ [x] ++ trosq elts_greq_x where elts_lt_x = [y | y <− xs, y < x] elts_greq_x = [y | y <− xs, y >= x] Ce programme est-il valide ? (syntaxe) Si oui, que fait-il ? (sémantique) Résumé 5 / 58 La syntaxe et les langages formels Les langages réguliers Trois règles de base : 1. enchaînement 2. alternative 3. fermeture de Kleene Exemple : les nombres entiers Tout ce qui s’exprime par ces règles appartient à un langage régulier correspondant à une expression régulière et est analysé par un scanneur. Important Langage formel (≠ langage de programmation) La syntaxe et les langages formels 7 / 58 Les expressions régulières Exemple d’une syntaxe : x xyz (…) M|N MN M* M+ M? […] [a–z] . un caractère (représente lui-même) une chaîne (représente elle-même) précédence M ou N M suivi par N zéro, une ou plusieurs instances de M une ou plusieurs instances de M zéro ou une instance de M n’importe quel caractère qui apparaît entre les crochets n’importe quel caractère entre a et z n’importe quel caractère La syntaxe et les langages formels 8 / 58 Quelques exemples • • • • • un nombre entier un mot clé d’un langage de programmation un identifiant d’un langage de programmation une expression avec des parenthèses équilibrées ? le code source d’un programme ? La syntaxe et les langages formels 9 / 58 Les langages hors-contexte Une quatrième règle de base [7] : 4. récursion Exemple : les expressions aux parenthèses équilibrées Tout ce qui s’exprime par ces quatre règles appartient à un langage hors-contexte (context free) correspondant à une grammaire hors-contexte et est analysé par un analyseur (parser). La syntaxe et les langages formels 10 / 58 Les grammaires EBNF “Extended Backus-Naur Form” [2] [3] [4] expression → number | ( expression ) expression → expression operator expression operator → + | − | * | / • Il y a une ou plusieurs productions pour chaque symbole non-terminal. • number est un symbole terminal. • Les opérateurs +, −, * et / sont des symboles littéraux. • expression est un symbole de départ. La syntaxe et les langages formels 11 / 58 Dériver une chaîne 8*4+3 La syntaxe et les langages formels 12 / 58 Un arbre syntaxique (parse tree) expression expression expression operator expression number * number operator expression + number La syntaxe et les langages formels 13 / 58 Une autre dérivation possible expression expression operator number * La syntaxe et les langages formels expression expression operator expression number + number 14 / 58 Une grammaire améliorée [5] [6] [7] [8] [9] expression term factor add_op mult_op → → → → → term | expression add_op term factor | term mult_op factor number | ( expression ) +|− *|/ Cette grammaire représente • l’associativité • la précédence 8*4+3 8−4−3 La syntaxe et les langages formels 15 / 58 Comment trouver une bonne grammaire ? Il y a un nombre infini de grammaires qui représentent un langage donné. On préfère des grammaires • qui représentent les règles d’évaluation (associativité, précédence, …) • qui réfléchissent la structure d’un programme d’une manière utile au compilateur (symboles nonterminaux tels que loop, statement, function) • dont l’analyse est simple et efficace La syntaxe et les langages formels 16 / 58 La syntaxe: Analyse lexicale et syntaxique La structure lexicale : • la structure des éléments individuels (jetons, tokens) (mots clés, identifiants, opérateurs, parenthèses, …) • spécifiée par des expressions régulières ou par des grammaires hors-contexte La structure syntaxique : • La composition de ces éléments • spécifiée par une grammaire hors-contexte Une formalisation astucieuse de la syntaxe facilite l’analyse automatique d’un programme. La syntaxe et les langages formels Jay et la syntaxe concrète 17 / 58 Jay – un langage simple // compute result – the nth Fibonacci number void main() { int n, fib0, fib1, temp, result; n = 8; fib0 = 0; fib1 = 1; while (n > 0) { temp = fib0; fib0 = fib1; fib1 = fib0 + temp; n = n – 1; } result = fib0; } Jay et la syntaxe concrète 19 / 58 La structure lexicale de Jay comment keyword identifier literal separator or_op and_op rel_op add_op mul_op not_op Jay et la syntaxe concrète //.*\n boolean | else | if | int | main | void | while [a–zA–Z_][a–zA–Z_0–9]* (avec des exceptions) true | false | [0–9]+ (|)|{|}|;|, || && < | <= | > | >= | == | != +|− *|/ ! 20 / 58 La syntaxe concrète de Jay [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] program → void main () { declarations statements } declarations → { declaration }* declaration → type identifiers ; type → int | boolean identifiers → identifier { , identifier }* statements → { statement }* statement → ; | block | assignment | if_stmt | while_stmt block → { statements } assignment → identifier = expression ; if_stmt → if ( expression ) statement { else statement }opt while_stmt → while ( expression ) statement Jay et la syntaxe concrète 21 / 58 La syntaxe concrète de Jay (suite) [21] [22] [23] [24] [25] [26] [27] expression → conjunction { or_op conjunction }* conjunction → relation { and_op relation }* relation → addition { rel_op addition }* addition → term { add_op term }* term → negation { mul_op negation }* negation → { neg_op }opt factor factor → identifier | literal | ( expression ) Jay et la syntaxe concrète 22 / 58 Un arbre de syntaxe concrète program void main ( ) { declarations statements declaration statement type identifiers ; int identifier identifier x x } assignment = expression ; literal 1 Jay et la syntaxe concrète 23 / 58 Que l’arbre représente-t-il ? • Le texte du programme apparaît de gauche à droite. • Une traversée post-ordre représente (plus ou moins) l’ordre de calcul. Que ne représente-t-il pas ? • Tous les identifiants ont-ils été déclarés ? • Sont-ils du bon type ? • Leur a-t-on assigné une valeur avant qu’ils ne soient utilisés ? • Si tout est correct, que signifie le programme ? La sémantique ! Jay et la syntaxe concrète 24 / 58 La grammaire de Jay est ambiguë ! if (true) if (true) ; else ; if_stmt if ( true ) if if_stmt statement ( true ) ; else ; if ( true ) statement else ; if ; ( true ) Jay et la syntaxe concrète 25 / 58 L’else suspendu (dangling else) Solutions : • exiger un mot clé pour fermer : if…fi (Ada) • spécification en dehors de la grammaire (Pascal, C, C++) • grammaire élaborée (Java) : [28] ifThenStmt → if ( expr ) statement [29] ifElseStmt → if ( expr ) stmtNoShortIf else statement Jay et la syntaxe concrète 26 / 58 Reconnaître de la syntaxe Analyse en deux étapes 1. La structure lexicale est spécifiée par un ensemble d’expressions régulières. Le scanneur transforme le flux de caractères d’un programme en un flux de jetons. 2. La syntaxe est spécifiée par une grammaire. L’analyseur transforme le flux de jetons en une représentation abstraite de la structure du programme. Chaque type de jetons correspond à un symbole terminal de la grammaire. Reconnaître de la syntaxe 28 / 58 Scanner avec un automate fini Les langages réguliers et les automates finis sont équivalents. • Un automate fini s’implémente très facilement. • Les cas spéciaux sont aisément pris en compte. • L’analyseur syntaxique demande des jetons au scanneur. • L’automate retourne toujours la plus longue chaîne possible. • Erreurs lexicales : • Comment se produisent-elles ? • Comment réagir ? Reconnaître de la syntaxe 29 / 58 Un automate fini partiel pour Jay space start / / \n Reconnaître de la syntaxe a-zA-Z_ a-zA-Z_0-9 0-9 0-9 + < = non-\n 30 / 58 Implémentation d’un scanneur lexical Token TokenStream::nextToken() { // Return token type and value skipWhiteSpace(); // first check for whitespace and bypass it // … // Check for an Operator; recover 2–character operators // as well as 1–character ones if (isOperator(nextChar)) { Token t("Operator", nextChar); switch (nextChar) { // look for <=, >=, !=, == case '<': case '>': case '!': case '=': nextChar = readChar(); if (nextChar == '=') { t.value += nextChar; nextChar = readChar(); } return t; // … default: // all other operators nextChar = readChar(); return t; Reconnaître de la syntaxe } } 31 / 58 Implémentation d’un scanneur lexical (suite) // Check for an Identifier, Keyword, or Literal if (isLetter(nextChar)) { Token t("Identifier"); while ((isLetter(nextChar) || isDigit(nextChar))) { t.value += nextChar; nextChar = readChar(); // get complete Identifier } if (isKeyword(t.value)) t.type = "Keyword"; // … (test for literal) return t; } // In case of a lexical error: … } Reconnaître de la syntaxe 32 / 58 Implémentation d’un scanneur lexical (suite) bool TokenStream::isOperator(char c) const { return (c == '=' || c == '+' || c == '–' || c == '*' || c == '/' || c == '<' || c == '>' || c == '!' || c == '&' || c == '|' ); } Reconnaître de la syntaxe 33 / 58 Analyse syntaxique Comment analyser un programme en termes de la grammaire ? • Plusieurs types de grammaires, plusieurs types d’analyseurs (→ cours en compilation) • La méthode la plus simple : L’analyse par descente récursive (recursive-descent parsing) Reconnaître de la syntaxe 34 / 58 L’analyse par descente récursive On traverse l’arbre de syntaxe concrète d’en haut : • Chaque symbole non-terminal est représenté par une méthode. • On reçoit du scanneur un jeton à la fois. • Selon le prochain jeton, on décide comment développer le nœud actuel. • En même temps, on construit la représentation désirée. Note Cela ne fonctionne pas avec n’importe quel type de grammaire ! Reconnaître de la syntaxe 35 / 58 Un exemple en Jay void main() { int f, i, n; n = 7; f = 1; i = 1; while (i < n) { i = i + 1; f = f * i; } } Reconnaître de la syntaxe 36 / 58 Comment construire un analyseur à descente récursive Pour chaque 1. non-terminal et chaque règle Pour chaque membre de : : a. 2. 3. Si est non-terminal, appeler la méthode correspondante. b. Si est terminal, et si la catégorie lexicale du jeton actuel est égale à , appeler nextToken() ; sinon, il constitue une erreur syntaxique. Si contient une série de symboles (indiquée par une ‘*’), insérer une boucle while correspondante. S’il y a plusieurs règles , insérer des if…else pour distinguer les possibilités selon le jeton actuel. Reconnaître de la syntaxe 37 / 58 Les différents types de grammaires et d’analyseurs LL( ), LR( ), … LL : LR : left-to-right, leftmost derivation (top-down) left-to-right, rightmost derivation (bottom-up) nombre de symboles consultés d’avance (lookahead) Le type d’une grammaire est donné par le type d’analyseur correspondant. • Notre analyseur par descente récursive est du type LL(1). • Nous sommes donc limités à des grammaires LL(1). Reconnaître de la syntaxe 38 / 58 LL et la récursion gauche [30] [31] [32] id_list → id id_list_tail id_list_tail → , id id_list_tail → ; La grammaire suivante accepte le même langage : [33] id_list → id_list_prefix ; [34] id_list_prefix → id_list_prefix , id [35] → id Elle n’est pas LL à cause d’une récursion gauche (left recursion). Elle se transforme mécaniquement en récursion droite. Reconnaître de la syntaxe 39 / 58 LL et les préfixes communs [36] [37] stmt → id := expr → id ( arg_list ) Ceci se résout mécaniquement, par factorisation gauche (left factoring) : [38] stmt → id stmt_tail [39] stmt_tail → := expr [40] → ( arg_list ) Mais s’il y a une autre production qui commence par le jeton ( ? Hélas, il y a des grammaires non-LL. Reconnaître de la syntaxe 40 / 58 Jay et la syntaxe abstraite Pourquoi analyser la syntaxe d’un programme ? Basé sur la syntaxe, on définit la logique de calcul, la sémantique du programme. Objectif : Lors de l’analyse syntaxique, créer une structure de données représentant cette logique. Jay et la syntaxe abstraite 42 / 58 Un arbre de syntaxe concrète assignment identifier = expression ; conjunction x relation addition term − term − term a b c Voir aussi la syntaxe abstraite [46]. Jay et la syntaxe abstraite 43 / 58 La syntaxe concrète… • pour spécifier le texte d’un programme • pour analyser le texte d’un programme • mais non appropriée pour l’analyse sémantique : • trop de détail • calcul mal représenté Jay et la syntaxe abstraite 44 / 58 … et la syntaxe abstraite • représente la structure du calcul (indépendamment du langage de programmation) • représentation intermédiaire pour • analyser la sémantique • optimisation • séparer le front end du back end Jay et la syntaxe abstraite 45 / 58 Un arbre de syntaxe abstraite Assignment Var Expr Variable Binary x Op ArithOp − Expr Expr Binary Op Expr Variable c Expr ArithOp Variable Variable − a b Voir aussi la syntaxe concrète [43]. Jay et la syntaxe abstraite 46 / 58 La représentation de la syntaxe abstraite Expression = Variable | Value | Binary | Unary Binary = Operator ; Expression ; Expression • des listes de composants (alternatifs ‘|’ ou combinés ‘;’) • construction en forme d’arbre Les composants correspondent (plus ou moins) aux composants de la syntaxe concrète. (Plutôt l’inverse !) Important Ceci n’est pas une grammaire (dérivations) mais une structure de données (composants). Jay et la syntaxe abstraite 47 / 58 La syntaxe abstraite de Jay Program Declarations Declaration Type Statement Block Assignment Conditional Loop Expression Variable Value Jay et la syntaxe abstraite = Declarations ; Block = Declaration* = Variable ; Type = int | boolean | undef = Skip | Block | Assignment | Conditional | Loop = Statement* = Variable ; Expression = Expression ; Statement ; Statement = Expression ; Statement = Variable | Value | Binary | Unary = string = int | boolean 48 / 58 La syntaxe abstraite de Jay (suite) Binary Unary Operator BoolOp RelOp ArithOp UnaryOp = Operator ; Expression ; Expression = UnaryOp ; Expression = BoolOp | RelOp | ArithOp | UnaryOp = && | || = < | <= | > | >= | == | != =+ | − | * | / =! Jay et la syntaxe abstraite 49 / 58 Implémenter la syntaxe abstraite class Binary : public Expression { Operator *op; Expression *term1, *term2; }; L’héritage représente naturellement la composition des éléments spécifiés par la syntaxe abstraite. Note • On ajoute des méthodes sémantiques, et voilà – un interpréteur ! • On ajoute des méthodes pour la génération de code, et voilà – un compilateur ! C’est cela l’objectif de la syntaxe abstraite – le lien entre la syntaxe et la sémantique. Jay et la syntaxe abstraite 50 / 58 Construire l’arbre de syntaxe abstraite lors de l’analyse Pour chaque : 1. 2. Créer une instance de la classe syntaxique abstraite correspondante. Pour chaque membre a. b. 3. non-terminal et pour chaque règle de : Si est non-terminal, appeler la méthode correspondante, et affecter sa valeur de retour au composant correspondant de . Si est terminal, et si la catégorie lexicale du jeton actuel est égale à , appeler nextToken() ; sinon, il constitue une erreur syntaxique. Si contient une série de symboles (indiquée par une ‘*’), insérer une boucle while correspondante. Jay et la syntaxe abstraite 51 / 58 Construire l’arbre de syntaxe abstraite lors de l’analyse (suite) 4. 5. S’il y a plusieurs règles , insérer des if…else pour distinguer les possibilités selon le jeton actuel. Retourner . Jay et la syntaxe abstraite 52 / 58 Exemple : Assignment Assignment* ConcreteSyntax::assignment() { // Assignment ––> Identifier = Expression ; Assignment* a = new Assignment(); if (token.type == "Identifier") { a–>target = new Variable(); a–>target–>id = token.value; token = input.nextToken(); match("="); a–>source = expression(); match(";"); } else syntaxError("Identifier"); return a; } Jay et la syntaxe abstraite 53 / 58 Exemple : Conjunction Expression* ConcreteSyntax::conjunction() { // Conjunction ––> Relation { && Relation }* Expression* e = relation(); while (token.value == "&&") { Binary* b = new Binary(); b–>term1 = e; b–>op = new Operator(token.value); token = input.nextToken(); b–>term2 = relation(); e = b; } return e; } Jay et la syntaxe abstraite 54 / 58 Exemple : Construire l’arbre de syntaxe abstraite Lors de l’analyse syntaxique : void main() { int f, i, n; n = 7; f = 1; i = 1; while (i < n) { i = i + 1; f = f * i; } } Jay et la syntaxe abstraite 55 / 58 Résumé Résumé • Spécifier la syntaxe : • expressions régulières • grammaires EBNF • Analyser la syntaxe : • automates finis • analyseurs par descente récursive • La syntaxe abstraite Résumé 57 / 58 Votre tâche Inventer et définir votre propre langage de programmation : • structure logique / sémantique (syntaxe abstraite) • structure lexicale (expressions régulières) et syntaxe concrète (grammaire EBNF) Soyez inventifs : • éléments de Pascal, C, C++, Lisp, langages script, … • boucles et conditionnels innovants ; opérateurs non standards, ternaires, … • deux types distincts au moins Flottants, chaînes, entrée/sortie simples possibles ; voir le site Web pour le support pour BSim. Pas de fonctions, structures de données complexes, … Résumé 58 / 58