web-dev-qa-db-fra.com

lexers vs analyseurs

Les lexers et les analyseurs syntaxiques sont-ils vraiment si différents en théorie?

Il semble à la mode de détester les expressions régulières: coding horror , n autre article de blog .

Cependant, les outils populaires basés sur le lexing: pygments , geshi , ou prettify , utilisent tous des expressions régulières. Ils semblent rien Lex ...

Quand le lexing est-il suffisant, quand avez-vous besoin d'EBNF?

Quelqu'un a-t-il utilisé les jetons produits par ces lexers avec des générateurs d'analyse de bison ou d'antir?

296
Naveen

Ce que les analyseurs et les lexers ont en commun:

  1. Ils lisent les symboles de certains alphabets à partir de leur entrée.

    • Astuce: L'alphabet ne doit pas nécessairement être en lettres. Mais il doit s'agir de symboles qui sont atomique pour le langage compris par l'analyseur/lexer.
    • Symboles pour le lexer: _ caractèresASCII.
    • Symboles pour l'analyseur: les jetons particuliers, qui sont des symboles terminaux de leur grammaire.
  2. Ils analysent ces symboles et tentent de les faire correspondre à la grammaire du langue ils ont compris.

    • Voici où réside la vraie différence. Voir ci-dessous pour plus.
    • Grammaire comprise par les lexers: grammaire régulière (niveau 3 de Chomsky).
    • Grammaire comprise par les analyseurs syntaxiques: grammaire sans contexte (niveau 2 de Chomsky).
  3. Ils attachent une sémantique (signification) aux éléments de langage trouvés.

    • Les lexers attribuent une signification en classifiant les lexèmes (chaînes de symboles de l'entrée) comme jetons particuliers . Par exemple. Tous ces lexèmes: *, ==, <=, ^ seront classés comme jeton "opérateur" par le lexer C/C++.
    • Les analyseurs attribuent une signification en classifiant les chaînes de jetons de l'entrée (phrases) en tant que non-finales particuliers et en construisant l'arbre d'analyse . . Par exemple. toutes ces chaînes de jetons: [number][operator][number], [id][operator][id], [id][operator][number][operator][number] seront classées comme "expression" non finales par l'analyseur C/C++.
  4. Ils peuvent attacher une signification supplémentaire (données) aux éléments reconnus.

    • Lorsqu'un lexer reconnaît une séquence de caractères constituant un nombre correct, il peut la convertir en valeur binaire et le stocker avec le jeton "nombre".
    • De même, lorsqu'un analyseur reconnaît une expression, il peut en calculer la valeur et le stocker avec le noeud "expression" de l'arbre de syntaxe.
  5. Ils produisent tous en sortie une phrase propre de la langue qu'ils reconnaissent.

    • Les lexers produisent des jetons , qui sont des phrases du langue habituelle qu'ils reconnaissent. Chaque jeton peut avoir une syntaxe interne (bien que le niveau 3, pas le niveau 2), mais cela n'a pas d'importance pour les données de sortie et pour celle qui les lit.
    • Les analyseurs produisent des arbres de syntaxe , qui sont des représentations de phrases du langage sans contexte qu’ils reconnaissent. Généralement, il ne s'agit que d'un seul grand arbre pour l'ensemble du document/fichier source, car l'ensemble du document/source est une phrase appropriée . Mais il n’existe aucune raison pour laquelle l’analyseur ne pourrait pas produire une série d’arbres de syntaxe sur sa sortie. Par exemple. ce pourrait être un analyseur qui reconnaît les balises SGML collées en texte brut. Donc, il va tokeniser le document SGML en une série de jetons: [TXT][TAG][TAG][TXT][TAG][TXT]....

Comme vous pouvez le constater, les analyseurs syntaxiques et les générateurs de jetons ont beaucoup en commun. Un analyseur peut être un jeton pour un autre analyseur, qui lit ses jetons d’entrée sous forme de symboles de son propre alphabet (les jetons sont simplement des symboles d’un alphabet) de la même manière que les phrases d’une langue peuvent être des symboles alphabétiques d’un autre, de niveau supérieur. la langue. Par exemple, si * et - sont les symboles de l'alphabet M (en tant que "symboles de code Morse"), vous pouvez créer un analyseur qui reconnaît les chaînes de ces points et de ces lignes en tant que lettres codées en morse. Les phrases dans la langue "Code Morse" pourraient être des jetons pour un autre analyseur, pour lesquels ces jetons sont des symboles atomiques de sa langue (par exemple, la langue "Mots anglais"). Et ces "mots anglais" pourraient être des jetons (symboles de l'alphabet) d'un analyseur syntaxique de niveau supérieur qui comprend la langue des "phrases anglaises". Et toutes ces langues ne diffèrent que par la complexité de la grammaire. Rien de plus.

Alors, que dire de ces "niveaux de grammaire de Chomsky"? Eh bien, Noam Chomsky a classé les grammaires en quatre niveaux en fonction de leur complexité:

  • Niveau 3: grammaires régulières

    Ils utilisent des expressions régulières, c’est-à-dire qu’ils ne peuvent contenir que les symboles de l’alphabet (a, b), leurs concaténations (ab, aba , bbb etd.), Ou des alternatives (par exemple a|b).
    Ils peuvent être implémentés comme automates à états finis (FSA), comme NFA (automate fini non déterministe) ou meilleur DFA (automate fini déterministe).
    Les grammaires normales ne peuvent pas gérer avec une syntaxe imbriquée , par exemple. parenthèses convenablement imbriquées/correspondantes (()()(()())), balises HTML/BBcode imbriquées, blocs imbriqués, etc. C'est parce que les automates à états pour les traiter doivent comporter un nombre infini d'états pour gérer un nombre infini de niveaux d'imbrication.
  • Niveau 2: grammaires sans contexte

    Ils peuvent avoir des branches imbriquées, récursives et auto-similaires dans leurs arborescences syntaxiques, afin de pouvoir gérer correctement les structures imbriquées.
    Ils peuvent être implémentés comme automate à états avec pile. Cette pile est utilisée pour représenter le niveau d'imbrication de la syntaxe. En pratique, ils sont généralement implémentés en tant qu'analyseur descendant et descendant récursif, qui utilise la pile d'appels de procédures de la machine pour suivre le niveau d'imbrication, et utilise des procédures/fonctions appelées récursivement pour chaque symbole non terminal dans leur syntaxe.
    Mais ils ne peuvent pas gérer avec une syntaxe contextuelle . Par exemple. quand vous avez une expression x+3 et que dans un contexte ceci x pourrait être le nom d'une variable, et dans un autre contexte cela pourrait être le nom d'une fonction, etc.
  • Niveau 1: grammaires contextuelles

  • Niveau 0: grammaires sans restriction
    Aussi appelée grammaire récursivement dénombrable.

455
SasQ

Oui, ils sont très différents en théorie et en implémentation.

Les lexers sont habitués à reconnaître les "mots" qui constituent des éléments de langage, car la structure de tels mots est généralement simple. Les expressions régulières sont extrêmement efficaces pour gérer cette structure simple, et il existe des moteurs de correspondance d'expression régulière très performants utilisés pour implémenter les lexeurs.

Les analyseurs syntaxiques sont utilisés pour reconnaître la "structure" d'une phrase. Une telle structure va généralement bien au-delà de ce que les "expressions régulières" peuvent reconnaître. Il faut donc utiliser des analyseurs syntaxiques "contextuels" pour extraire cette structure. Les analyseurs contextuels étant difficiles à créer, le compromis en matière d'ingénierie consiste à utiliser des grammaires "sans contexte" et à ajouter des hacks aux analyseurs syntaxiques ("tables de symboles", etc.) pour gérer la partie sensible au contexte.

Ni lexing ni la technologie d'analyse ne vont probablement disparaître bientôt.

Ils peuvent être unifiés en décidant d'utiliser la technologie "d'analyse syntaxique" pour reconnaître les "mots", comme l'expliquent actuellement les soi-disant analyseurs GLR sans scanner. Cela a un coût d'exécution, car vous appliquez des machines plus générales à ce qui est souvent un problème qui n'en a pas besoin et que vous payez généralement en frais généraux. Lorsque vous avez beaucoup de cycles gratuits, ces frais généraux ne sont peut-être pas importants. Si vous traitez beaucoup de texte, les frais généraux sont importants et les analyseurs syntaxiques d'expression classiques continueront d'être utilisés.

100
Ira Baxter

Quand le lexing est-il suffisant, quand avez-vous besoin d'EBNF?

EBNF n'ajoute pas grand chose à la puissance des grammaires. C'est juste une commodité/notation de raccourci/"sucre syntaxique" sur les règles de grammaire standard de Chomsky, la forme normale (CNF). Par exemple, l’alternative EBNF:

S --> A | B

vous pouvez obtenir en CNF en énumérant simplement chaque production alternative:

S --> A      // `S` can be `A`,
S --> B      // or it can be `B`.

L'élément optionnel de EBNF:

S --> X?

vous pouvez obtenir dans CNF en utilisant une production nullable, c'est-à-dire celle qui peut être remplacée par un chaîne vide (indiquée par une production simplement vide ici; d'autres utilisent epsilon ou lambda ou cercle croisé):

S --> B       // `S` can be `B`,
B --> X       // and `B` can be just `X`,
B -->         // or it can be empty.

Une production sous une forme comme la dernière B ci-dessus est appelée "effacement", car elle peut effacer ce qu'elle représente dans d'autres productions (produit une chaîne vide au lieu de quelque chose d'autre).

Zéro ou plus répétition de EBNF:

S --> A*

vous pouvez obtenir en utilisant la production récursive, c'est-à-dire celle qui s'y intègre quelque part. Cela peut être fait de deux manières. Le premier est récursion gauche (ce qui devrait généralement être évité, car les analyseurs syntaxiques descendant récursifs ne peuvent pas l’analyser):

S --> S A    // `S` is just itself ended with `A` (which can be done many times),
S -->        // or it can begin with empty-string, which stops the recursion.

Sachant qu’elle ne génère qu’une chaîne vide (au final) suivie de zéro ou plus de As, la même chaîne (mais pas le même langage!) peut être exprimée à l’aide de right- récursion:

S --> A S    // `S` can be `A` followed by itself (which can be done many times),
S -->        // or it can be just empty-string end, which stops the recursion.

Et quand il s’agit de + pour une ou plusieurs répétitions d’EBNF:

S --> A+

cela peut être fait en factorisant une A et en utilisant * comme auparavant:

S --> A A*

que vous pouvez exprimer en tant que tel dans CNF (j’utilise la bonne récursion ici; essayez de trouver l’autre comme exercice):

S --> A S   // `S` can be one `A` followed by `S` (which stands for more `A`s),
S --> A     // or it could be just one single `A`.

Sachant cela, vous pouvez maintenant probablement reconnaître une grammaire pour une expression régulière (c'est-à-dire grammaire régulière) comme pouvant être exprimée dans une production EBNF unique composée uniquement de symboles terminaux. Plus généralement, vous pouvez reconnaître les grammaires régulières lorsque vous voyez des productions similaires à celles-ci:

A -->        // Empty (nullable) production (AKA erasure).
B --> x      // Single terminal symbol.
C --> y D    // Simple state change from `C` to `D` when seeing input `y`.
E --> F z    // Simple state change from `E` to `F` when seeing input `z`.
G --> G u    // Left recursion.
H --> v H    // Right recursion.

C'est-à-dire que vous n'utilisez que des chaînes vides, des symboles de terminal, de simples non-terminaux pour les substitutions et les changements d'état, et que vous utilisez la récursivité uniquement pour obtenir une répétition (itération, qui est simplement récurrence linéaire - celle qui ne fonctionne branche semblable à un arbre). Rien de plus avancé au-dessus de ceux-ci, alors vous êtes sûr que c'est une syntaxe normale et vous pouvez utiliser simplement lexer pour cela.

Mais lorsque votre syntaxe utilise la récursivité de manière non triviale, vous créez des structures imbriquées semblables à des arbres, similaires à celles-ci, telles que la suivante:

S --> a S b    // `S` can be itself "parenthesized" by `a` and `b` on both sides.
S -->          // or it could be (ultimately) empty, which ends recursion.

alors vous pouvez facilement voir que cela ne peut pas être fait avec une expression régulière, parce que vous ne pouvez pas le résoudre en une seule production EBNF de quelque manière que ce soit; vous finirez par substituer indéfiniment S, ce qui ajoutera toujours un autre as et bs des deux côtés. Les lexers (plus particulièrement: les automates à états finis utilisés par les lexers) ne peuvent compter en nombres arbitraires (ils sont finis, rappelez-vous?), Ils ne savent donc pas combien de as étaient là pour les faire correspondre de manière égale à tant de bs. Les grammaires comme celui-ci s'appellent grammaires dépourvues de contexte (au minimum), et ils nécessitent un analyseur.

Les grammaires sans contexte sont bien connues pour être analysées. Elles sont donc largement utilisées pour décrire la syntaxe des langages de programmation. Mais il y a plus. Parfois, une grammaire plus générale est nécessaire - lorsque vous avez plus de choses à compter en même temps, indépendamment. Par exemple, lorsque vous souhaitez décrire une langue dans laquelle vous pouvez utiliser des parenthèses arrondies et des accolades carrées entrelacées, mais elles doivent être associées correctement les unes aux autres (accolades avec accolades, arrondies avec arrondies). Ce type de grammaire s'appelle sensible au contexte. Vous pouvez le reconnaître par le fait qu’il a plus d’un symbole à gauche (avant la flèche). Par exemple:

A R B --> A S B

Vous pouvez considérer ces symboles supplémentaires à gauche comme un "contexte" pour l'application de la règle. Par exemple, la règle ci-dessus substituera R à S, mais seulement si c'est entre A et B, laissant ces A et B eux-mêmes inchangés. Ce type de syntaxe est vraiment difficile à analyser, car il nécessite une machine complète de Turing. C'est une toute autre histoire, alors je vais terminer ici.

30
SasQ

Répondre à la question telle que posée (sans répéter indûment ce qui apparaît dans les autres réponses)

Lexers et parseurs ne sont pas très différents, comme le suggère la réponse acceptée. Les deux sont basés sur des formalismes de langage simples: des langages standard pour les lexers et, presque toujours, des langages sans contexte (CF) pour les analyseurs. Ils sont tous deux associés à des modèles de calcul relativement simples, à l'automate à états finis et à l'automate à pile Push-down. Les langues ordinaires sont un cas particulier de langues sans contexte, de sorte que des lexers puissent être produits avec la technologie CF un peu plus complexe. Mais ce n’est pas une bonne idée pour au moins deux raisons.

Un point fondamental de la programmation est qu’un composant du système doit être doté de la technologie la plus appropriée, de manière à ce qu’il soit facile à produire, à comprendre et à entretenir. La technologie ne doit pas être exagérée (utilisation de techniques beaucoup plus complexes et coûteuses que nécessaire), ni à la limite de sa puissance, ce qui nécessite des contorsions techniques pour atteindre l'objectif souhaité.

C'est pourquoi "il semble à la mode de détester les expressions régulières". Bien qu'ils puissent faire beaucoup, ils ont parfois besoin d'un code très illisible pour le réaliser, sans oublier le fait que diverses extensions et restrictions de mise en œuvre réduisent quelque peu leur simplicité théorique. Les Lexers ne le font généralement pas et constituent généralement une technologie simple, efficace et appropriée pour analyser les jetons. Utiliser des analyseurs syntaxiques des FC pour les jetons serait excessif, bien que ce soit possible.

Une autre raison de ne pas utiliser le formalisme des FC pour les lexistes est qu’il pourrait alors être tentant d’utiliser toute la puissance des FC. Mais cela pourrait poser des problèmes de structure en ce qui concerne la lecture des programmes.

Fondamentalement, la majeure partie de la structure du texte du programme, à partir de laquelle le sens est extrait, est une structure arborescente. Il exprime comment la phrase d'analyse (programme) est générée à partir de règles de syntaxe. La sémantique est dérivée par des techniques de composition (homomorphisme pour les orientés mathématiquement) de la manière dont les règles de syntaxe sont composées pour construire l'arbre d'analyse. Par conséquent, la structure arborescente est essentielle. Le fait que les jetons soient identifiés avec un lexer régulier basé sur un ensemble ne change pas la situation, car CF composé avec régulière donne toujours CF (je parle très vaguement des transducteurs réguliers, qui transforment un flux de caractères en un flux de jetons).

Cependant, la mucoviscidose composée avec CF (via des transducteurs CF ... désolé pour le calcul), ne donne pas nécessairement CF, et pourrait rendre les choses plus générales, mais moins faciles à gérer en pratique. Donc, CF n'est pas l'outil approprié pour les lexeurs, même s'il peut être utilisé.

L’une des différences majeures entre les langues classiques et les FC est que les langages normaux (et les transducteurs) se comportent très bien avec presque tous les formalismes de diverses manières, alors que les langages des FC (et les transducteurs) ne le font pas, même avec eux-mêmes (à quelques exceptions près).

(Notez que les transducteurs classiques peuvent avoir d'autres utilisations, telles que la formalisation de certaines techniques de traitement des erreurs de syntaxe.)

BNF n'est qu'une syntaxe spécifique pour présenter les grammaires des FC.

EBNF est un sucre syntaxique pour BNF , utilisant les fonctionnalités de la notation normale pour donner une version plus précise des grammaires BNF. Il peut toujours être transformé en une BNF pure équivalente.

Cependant, la notation régulière est souvent utilisée dans EBNF uniquement pour souligner ces parties de la syntaxe qui correspondent à la structure des éléments lexicaux et doivent être reconnues avec le lexer, le reste étant plutôt présenté en BNF simple. Mais ce n'est pas une règle absolue.

Pour résumer, la structure plus simple du jeton est mieux analysée avec la technologie plus simple des langages normaux, tandis que la structure arborescente du langage (de la syntaxe du programme) est mieux gérée par les grammaires CF

Je suggérerais également de regarder réponse de AHR .

Mais cela laisse une question ouverte: Pourquoi des arbres?

Les arbres sont une bonne base pour spécifier la syntaxe car

  • ils donnent une structure simple au texte

  • il est très pratique d'associer la sémantique au texte sur la base de cette structure, avec une technologie bien comprise sur le plan mathématique (compositionnalité via des homomorphismes), comme indiqué ci-dessus. C'est un outil algébrique fondamental pour définir la sémantique des formalismes mathématiques.

C'est donc une bonne représentation intermédiaire, comme en témoigne le succès des arbres de syntaxe abstraite (AST). Notez que AST sont souvent différents de l'arbre d'analyse car la technologie d'analyse utilisée par de nombreux professionnels (tels que LL ou LR) s'applique uniquement à un sous-ensemble de grammaires CF, ce qui entraîne des distorsions grammaticales qui sont corrigées ultérieurement dans AST. Ceci peut être évité avec une technologie d'analyse plus générale (basée sur une programmation dynamique) qui accepte toute grammaire CF.

Les déclarations sur le fait que les langages de programmation sont sensibles au contexte (CS) plutôt que CF sont arbitraires et discutables.

Le problème est que la séparation de la syntaxe et de la sémantique est arbitraire. Le contrôle des déclarations ou des accords de type peut être considéré comme faisant partie de la syntaxe ou de la sémantique. Il en irait de même de l’accord de genre et de nombre dans les langues naturelles. Mais il existe des langages naturels dans lesquels un accord pluriel dépend de la signification sémantique réelle des mots, de sorte qu'il ne correspond pas à la syntaxe.

De nombreuses définitions de langages de programmation en sémantique dénotationnelle placent des déclarations et des vérifications de type dans la sémantique. Donc, affirmant que cela est fait par Ira Baxter que les analyseurs syntaxiques des FC sont piratés pour obtenir la sensibilité au contexte requise par la syntaxe est au mieux une vue arbitraire de la situation. Cela peut être organisé comme un hack dans certains compilateurs, mais ce n’est pas nécessairement le cas.

De plus, il n’est pas seulement que les analyseurs syntaxiques CS (au sens utilisé dans d’autres réponses ici) sont difficiles à construire et moins efficaces. Ils sont également insuffisants pour exprimer clairement le type de sensibilité au contexte qui pourrait être nécessaire. Et ils ne produisent pas naturellement une structure syntaxique (telle que des arbres d’analyse) qui soit pratique pour dériver la sémantique du programme, c’est-à-dire pour générer le code compilé.

11
babou

Il existe plusieurs raisons pour lesquelles la partie analyse d'un compilateur est normalement séparée en phases d'analyse lexicale et d'analyse (analyse syntaxique).

  1. La simplicité de conception est la considération la plus importante. La séparation des analyses lexicale et syntaxique nous permet souvent de simplifier au moins une de ces tâches. Par exemple, un analyseur qui doit traiter des commentaires et des espaces en tant qu'unités syntaxiques le serait. Beaucoup plus complexe que celui qui peut supposer que les commentaires et les espaces ont déjà été supprimés par l'analyseur lexical. Si nous concevons un nouveau langage, la séparation des préoccupations lexicales et syntaxiques peut conduire à une conception globale du langage plus propre.
  2. L'efficacité du compilateur est améliorée. Un analyseur lexical séparé nous permet d’appliquer des techniques spécialisées qui ne servent que la tâche lexicale, pas le travail d’analyse. De plus, des techniques de mise en mémoire tampon spécialisées pour la lecture des caractères d'entrée peuvent accélérer considérablement le compilateur.
  3. La portabilité du compilateur est améliorée. Les particularités propres au périphérique d'entrée peuvent être limitées à l'analyseur lexical.

ressource ___Compilateurs (2e édition) écrit par- Alfred V. Abo Université Columbia Monica S. Lam Université Stanford Ravi Sethi Avaya Jeffrey D. Ullman Université Stanford

6
AHR