web-dev-qa-db-fra.com

Quelle est la procédure suivie lors de la rédaction d'un lexer basé sur une grammaire?

Tout en lisant une réponse à la question Clarification sur les grammaires, Lexers and Parsers , la réponse indiquait que:

[...] Une grammaire BNF contient toutes les règles dont vous avez besoin pour une analyse lexicale et une analyse.

Cela est tombé en face comme un peu étrange de moi parce que jusqu'à maintenant, j'avais toujours pensé qu'un lexer était pas Basé sur une grammaire du tout, tandis qu'un analyseur était fortement basé sur une. J'étais venu à cette conclusion après avoir lu de nombreux blogs Post sur l'écriture de Lexers, et non jamais l'utilisation de 1EBNF/BNF comme base pour la conception.

Si les lexers, ainsi que les analystes, sont basés sur une grammaire EBNF/BNF, alors comment créerait-il de créer un lexer à l'aide de cette méthode? C'est-à-dire comment construirais-je un lexer à l'aide d'une grammaire EBNF/BNF donnée?

J'ai vu beaucoup, Beaucoup Post qui traite de la rédaction d'un analyseur à l'aide d'EBNF/BNF en tant que guide ou un plan, mais je ne suis pas surmonté jusqu'à présent qui montrent l'équivalent avec Lexer Design .

Par exemple, prenez la grammaire suivante:

input = digit| string ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"', { all characters - '"' }, '"' ;
all characters = ? all visible characters ? ;

Comment créerait-on un lexer qui est basé sur la grammaire? Je pouvais imaginer comment un analyseur pouvait être écrit d'une telle grammaire, mais je ne parviens pas à saisir le concept de faire la même chose avec un Lexer.

Existe-t-il certaines règles ou logiques utilisées pour accomplir une tâche telle que celle-ci, comme pour la rédaction d'un analyseur? Franchement, je commence à me demander si Lexer Designs utilise du tout une grammaire EBNF/BNF, encore moins basées sur une.


1formulaire de backus-naur étend et formulaire de backus-naur

13
Christian Dean

Les Lexers ne sont que des analyseurs simples qui sont utilisés comme optimisation de la performance pour l'analyseur principal. Si nous avons un Lexer, le Lexer et l'analyseur travaillent ensemble pour décrire la langue complète. Les analysaires qui n'ont pas de stade de lexing séparé sont parfois appelés "scannerless".

Sans Lexers, l'analyseur devrait fonctionner sur une base de caractère par caractère. Étant donné que l'analyseur doit stocker des métadonnées à propos de chaque élément d'entrée, il peut être nécessaire de pré-calculer des tableaux pour chaque état de l'élément d'entrée, cela entraînerait une consommation de mémoire inacceptable pour les grandes tailles d'entrée. En particulier, nous n'avons pas besoin d'un nœud distinct par caractère dans l'arbre de syntaxe abstraite.

Étant donné que le texte sur une base de caractère par caractère est assez ambigu, cela entraînerait également une ambiguïté beaucoup plus ennuyeuse à gérer. Imaginez une règle R → identifier | "for " identifier. où -identifiant est composé de ASCII lettres. Si je veux éviter toute ambiguïté, j'ai maintenant besoin d'un lookahead de 4 caractères pour déterminer quelle alternative doit être choisie. Avec un Lexer, l'analyseur doit simplement vérifier s'il a un identifiant ou pour un jeton - un lookahead à 1 jeton.

Grammaires à deux niveaux.

Les Lexers fonctionnent en traduisant l'alphabet d'entrée sur un alphabet plus pratique.

Un analyseur sans scanner décrit une grammaire (N, σ, P, S) où les non-terminaux n sont les côtés gauche des règles dans la grammaire, l'alphabet σ est par exemple. ASCII caractères, les productions P sont les règles de la grammaire et le symbole de démarrage S est la règle de niveau supérieur de l'analyseur.

Le Lexer définit maintenant un alphabet de jetons A, B, C, .... Cela permet à l'analyseur principal d'utiliser ces jetons comme alphabet: σ = {A, B, C, ...}. Pour le LXER, ces jetons sont des non-terminaux et la règle de départ SL est sL → ε | a s | b s | C S | ..., c'est: toute séquence de jetons. Les règles de la grammaire Lexer sont toutes les règles nécessaires pour produire ces jetons.

L'avantage de la performance vient d'exprimer les règles de Lexer en tant que langue régulière. Celles-ci peuvent être analysées beaucoup plus efficacement que les langues libres de contexte. En particulier, des langues ordinaires peuvent être reconnues dans O(n) espace et O(n) heure. Dans la pratique, un générateur de code peut transformer un tel lexer. dans des tables de saut très efficaces.

Extraire des jetons de votre grammaire.

Pour toucher votre exemple: les règles digit et string sont exprimées sur un niveau de caractère par caractère. Nous pourrions les utiliser comme jetons. Le reste de la grammaire reste intact. Voici la grammaire Lexer, écrite comme une grammaire linéaire droite pour indiquer clairement que c'est régulier:

digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"' , string-rest ;
string-rest = '"' | STRING-CHAR, string-rest ;
STRING-CHAR = ? all visible characters ? - '"' ;

Mais comme il est régulier, nous utiliserions généralement des expressions régulières pour exprimer la syntaxe de jeton. Voici les définitions de jeton ci-dessus en tant que regexes, écrites à l'aide de la syntaxe d'exclusion de la classe de caractères .NET et de POSIX Charclasses:

digit ~ [0-9]
string ~ "[[:print:]-["]]*"

La grammaire pour l'analyseur principal contient ensuite les règles restantes non gérées par le LXER. Dans votre cas, c'est juste:

input = digit | string ;

Lorsque Lexers ne peut pas être utilisé facilement.

Lors de la conception d'une langue, nous veillons généralement à ce que la grammaire puisse être séparée proprement en un niveau lexère et un niveau d'analyseur, et que le niveau LXER décrit une langue régulière. Ce n'est pas toujours possible.

  • Lors de l'intégration des langues. Certaines langues vous permettent d'interpoler le code dans les chaînes: "name={expression}". La syntaxe d'expression fait partie de la grammaire sans contexte et ne peut donc pas être tokenized par une expression régulière. Pour résoudre ce problème, nous recombonsons l'analyseur avec le LXER, ou nous introduisons des jetons supplémentaires comme STRING-CONTENT, INTERPOLATE-START, INTERPOLATE-END. La règle de la grammaire pour une chaîne pourrait alors ressembler à: String → STRING-START STRING-CONTENTS { INTERPOLATE-START Expression INTERPOLATE-END STRING-CONTENTS } STRING-END. Bien sûr, l'expression peut contenir d'autres cordes, ce qui nous conduit au problème suivant.

  • Quand les jetons pourraient se contenir. Dans les langues de type C, les mots-clés sont indiscernables des identifiants. Ceci est résolu dans la Lexer en priorisant des mots-clés sur des identificateurs. Une telle stratégie n'est pas toujours possible. Imaginez un fichier de configuration où Line → IDENTIFIER " = " REST, Où le reste est n'importe quel caractère jusqu'à la fin de la ligne, même si le reste ressemble à un identifiant. Un exemple de ligne serait a = b c. Le Lexer est vraiment stupide et ne sait pas dans quel ordre les jetons peuvent survenir. Donc, si nous donnons la priorité à l'identifiant sur le repos, le Lexer nous donnerait IDENT(a), " = ", IDENT(b), REST( c). Si nous priorisons REST Over Identifiant, le Lexer nous donnerait simplement REST(a = b c).

    Pour résoudre ceci, nous devons recombiner le Lexer avec l'analyseur. La séparation peut être maintenue quelque peu en faisant le lexer paresseux: chaque fois que l'analyseur a besoin de la prochaine jeton, il le demande à Lexer et indique à la LXER l'ensemble des jetons acceptables. Effectivement, nous créons une nouvelle règle de haut niveau pour la grammaire Lexer pour chaque position. Ici, cela entraînerait les appels nextToken(IDENT), nextToken(" = "), nextToken(REST), et tout fonctionne bien. Cela nécessite un analyseur qui connaît l'ensemble complet de jetons acceptables à chaque emplacement, ce qui implique un analyseur de bas en haut comme LR.

  • Lorsque le LXER doit maintenir l'état. Par exemple. Le Python Language délimite les blocs de code non pas par des accolades bouclés, mais par l'indentation. Il existe des moyens de gérer la syntaxe sensible à la mise en page dans une grammaire, mais ces techniques sont surchargées pour Python. Au lieu de cela, les contrôles Lexer. l'indentation de chaque ligne et émet des jetons d'indent si un nouveau bloc en retrait est trouvé et des jetons de dédentaient si le bloc est terminé. Cela simplifie la grammaire principale car elle peut maintenant prétendre que ces jetons sont comme des accolades bouclées. Le Lexer doit toutefois maintenant maintenir l'état: l'indentation actuelle. Cela signifie que le Lexer techniquement ne décrit pas plus une langue régulière, mais en fait une langue contextuelle. Heureusement, cette différence n'est pas pertinente dans la pratique et que Python's Lexer peut toujours fonctionner dans O(n) heure.

18
amon