Quel devrait être le type de données des jetons qu'un lexeur renvoie à son analyseur?

21

Comme indiqué dans le titre, quel type de données un lexer doit-il retourner / donner à l'analyseur? En lisant l' article sur l' analyse lexicale de Wikipédia, il a déclaré que:

En informatique, l'analyse lexicale est le processus de conversion d'une séquence de caractères (comme dans un programme informatique ou une page Web) en une séquence de jetons ( chaînes avec une «signification» identifiée).

Cependant, en totale contradiction avec la déclaration ci-dessus, lorsqu'une autre question que j'ai posée sur un autre site ( Code Review si vous êtes curieux) a été répondue, la personne qui a répondu a déclaré que:

Le lexer lit généralement la chaîne et le convertit en un flux ... de lexèmes. Les lexèmes doivent seulement être un flux de nombres .

et il a donné ce visuel:

nl_output => 256
output    => 257
<string>  => 258

Plus Flexloin dans l'article, Il a mentionné , un lexer déjà existant, et a dit que l'écriture de «règles» avec lui serait plus simple que d'écrire un lexer à la main. Il a ensuite donné cet exemple:

Space              [ \r\n\t]
QuotedString       "[^"]*"
%%
nl_output          {return 256;}
output             {return 257;}
{QuotedString}     {return 258;}
{Space}            {/* Ignore */}
.                  {error("Unmatched character");}
%%

Pour approfondir mes connaissances et obtenir plus d'informations, j'ai lu l'article Wikipedia sur Flex . l'article Flex a montré que vous pouviez définir un ensemble de règles de syntaxe, avec des jetons, de la manière suivante:

digit         [0-9]
letter        [a-zA-Z]

%%
"+"                  { return PLUS;       }
"-"                  { return MINUS;      }
"*"                  { return TIMES;      }
"/"                  { return SLASH;      }
"("                  { return LPAREN;     }
")"                  { return RPAREN;     }
";"                  { return SEMICOLON;  }
","                  { return COMMA;      }
"."                  { return PERIOD;     }
":="                 { return BECOMES;    }
"="                  { return EQL;        }
"<>"                 { return NEQ;        }
"<"                  { return LSS;        }
">"                  { return GTR;        }
"<="                 { return LEQ;        }
">="                 { return GEQ;        }
"begin"              { return BEGINSYM;   }
"call"               { return CALLSYM;    }
"const"              { return CONSTSYM;   }
"do"                 { return DOSYM;      }
"end"                { return ENDSYM;     }
"if"                 { return IFSYM;      }
"odd"                { return ODDSYM;     }
"procedure"          { return PROCSYM;    }
"then"               { return THENSYM;    }
"var"                { return VARSYM;     }
"while"              { return WHILESYM;   }

Il me semble que le lexer Flex renvoie des chaînes de mots-clés \ tokens. Mais il pourrait s'agir de constantes de retour égales à certains nombres.

Si le lexer devait retourner des nombres, comment lirait-il les littéraux de chaîne? renvoyer un nombre est très bien pour des mots clés uniques, mais comment traiteriez-vous une chaîne? Le lexeur n'aurait-il pas à convertir la chaîne en nombres binaires, puis l'analyseur reconvertirait les nombres en chaîne. Il semble beaucoup plus logique (et plus facile) pour le lexer de renvoyer des chaînes, puis de laisser l'analyseur convertir les littéraux de chaîne de nombres en nombres réels.

Ou le lexer pourrait-il retourner les deux? J'ai essayé d'écrire un lexer simple en c ++, qui vous permet d'avoir un seul type de retour pour vos fonctions. Me conduisant ainsi à poser ma question.

Pour condenser ma question en un paragraphe: lors de l'écriture d'un lexer, et en supposant qu'il ne pourrait renvoyer qu'un seul type de données (chaînes ou nombres), quel serait le choix le plus logique?

Christian Dean
la source
Le lexer renvoie ce que vous lui demandez de retourner. Si votre conception appelle des numéros, elle renverra des numéros. Évidemment, représenter des littéraux de chaîne va exiger un peu plus que cela. Voir aussi Est-ce le travail de Lexer d'analyser des nombres et des chaînes? Notez que les littéraux de chaîne ne sont généralement pas considérés comme des «éléments de langage».
Robert Harvey
@RobertHarvey Pourriez-vous convertir le littéral de chaîne en nombres binaires?.
Christian Dean
Si je comprends bien, le but du lexer est de prendre les éléments du langage (comme les mots-clés, les opérateurs, etc.) et de les transformer en jetons. En tant que telles, les chaînes entre guillemets n'intéressent pas le lexeur, car elles ne sont pas des éléments de langage. Bien que je n'ai jamais écrit de lexer moi-même, j'imagine que la chaîne entre guillemets est simplement passée sans modification (y compris les guillemets).
Robert Harvey
Donc, ce que vous dites, c'est que le lexer ne lit pas ou ne se soucie pas des littéraux de chaîne. Et donc l'analyseur doit rechercher ces littéraux de chaîne? C'est très déroutant.
Christian Dean
Vous voudrez peut-être passer quelques minutes à lire ceci: en.wikipedia.org/wiki/Lexical_analysis
Robert Harvey

Réponses:

10

Généralement, si vous traitez une langue par lexing et analyse, vous avez une définition de vos jetons lexicaux, par exemple:

NUMBER ::= [0-9]+
ID     ::= [a-Z]+, except for keywords
IF     ::= 'if'
LPAREN ::= '('
RPAREN ::= ')'
COMMA  ::= ','
LBRACE ::= '{'
RBRACE ::= '}'
SEMICOLON ::= ';'
...

et vous avez une grammaire pour l'analyseur:

STATEMENT ::= IF LPAREN EXPR RPAREN STATEMENT
            | LBRACE STATEMENT BRACE
            | EXPR SEMICOLON
EXPR      ::= ID
            | NUMBER
            | ID LPAREN EXPRS RPAREN
...

Votre lexer prend le flux d'entrée et produit un flux de jetons. Le flux de jetons est consommé par l'analyseur pour produire un arbre d'analyse. Dans certains cas, il suffit de connaître le type de jeton (par exemple, LPAREN, RBRACE, FOR), mais dans certains cas, vous aurez besoin de la valeur réelle associée au jeton. Par exemple, lorsque vous rencontrez un jeton d'ID, vous voudrez les caractères réels qui composent l'ID plus tard lorsque vous essayez de déterminer l'identifiant que vous essayez de référencer.

Donc, vous avez généralement quelque chose de plus ou moins comme ceci:

enum TokenType {
  NUMBER, ID, IF, LPAREN, RPAREN, ...;
}

class Token {
  TokenType type;
  String value;
}

Ainsi, lorsque le lexer renvoie un jeton, vous savez de quel type il s'agit (dont vous avez besoin pour l'analyse) et de la séquence de caractères à partir de laquelle il a été généré (dont vous aurez besoin plus tard pour interpréter les chaînes et les littéraux numériques, les identifiants, etc.). Il peut sembler que vous renvoyez deux valeurs, car vous renvoyez un type d'agrégat très simple, mais vous avez vraiment besoin des deux parties. Après tout, vous voudriez traiter différemment les programmes suivants:

if (2 > 0) {
  print("2 > 0");
}
if (0 > 2) {
  print("0 > 2");
}

Ceux-ci produisent la même séquence de types de jetons : IF, LPAREN, NUMBER, GREATER_THAN, NUMBER, RPAREN, LBRACE, ID, LPAREN, STRING, RPAREN, SEMICOLON, RBRACE. Cela signifie qu'ils analysent également la même chose. Mais lorsque vous faites réellement quelque chose avec l'arbre d'analyse, vous vous soucierez que la valeur du premier nombre est '2' (ou '0') et que la valeur du deuxième nombre soit '0' (ou '2 ') et que la valeur de la chaîne est' 2> 0 '(ou' 0> 2 ').

Joshua Taylor
la source
Je comprends l'essentiel de ce que vous dites, mais comment cela String valueva-t-il être rempli? sera-t-il rempli d'une chaîne ou d'un nombre? Et aussi, comment définir le Stringtype?
Christian Dean
1
@ Mr.Python Dans le cas le plus simple, c'est juste la chaîne de caractères qui correspond à la production lexicale. Donc, si vous voyez foo (23, "bar") , vous obtiendrez les jetons [ID, "foo"], [LPAREN, "(" ", [NUMBER," 23 "], [COMMA,", " ], [STRING, "" 23 ""], [RPAREN, ")"] . La conservation de ces informations pourrait être importante. Ou vous pouvez adopter une autre approche et faire en sorte que la valeur ait un type d'union qui peut être une chaîne, ou un nombre, etc., et choisir le bon type de valeur en fonction du type de type de jeton que vous avez (par exemple, lorsque le type de jeton est NUMBER) , utilisez value.num et lorsqu'il s'agit de STRING, utilisez value.str).
Joshua Taylor
@MrPython "Et aussi, comment définir le type de chaîne?" J'écrivais à partir d'un état d'esprit Java. Si vous travaillez en C ++, vous pouvez utiliser le type de chaîne de C ++, ou si vous travaillez en C, vous pouvez utiliser un char *. Le point est celui associé à un jeton, vous avez la valeur correspondante, ou le texte que vous pouvez interpréter pour produire la valeur.
Joshua Taylor
1
@ ollydbg23 c'est une option, et non pas déraisonnable, mais cela rend le système moins cohérent en interne. Par exemple, si vous voulez la valeur de chaîne de la dernière ville que vous avez analysée, vous devez maintenant vérifier explicitement une valeur nulle, puis utiliser une recherche inversée de jeton à chaîne pour savoir quelle aurait été la chaîne. De plus, c'est un couplage plus étroit entre le lexer et l'analyseur; il y a plus de code à mettre à jour si LPAREN peut jamais correspondre à des chaînes différentes ou multiples.
Joshua Taylor
2
@ ollydbg23 Un cas serait un simple pseudo-minifieur. C'est assez facile à faire parse(inputStream).forEach(token -> print(token.string); print(' '))(c'est- à- dire qu'il suffit d'imprimer les valeurs de chaîne des jetons, séparées par un espace). C'est assez rapide. Et même si LPAREN ne peut provenir que de "(", cela pourrait être une chaîne constante en mémoire, donc y inclure une référence dans le jeton pourrait ne pas coûter plus cher que d'inclure la référence nulle. En général, je préfère écrire code qui ne fait pas de moi un cas spécial
Joshua Taylor
6

Comme indiqué dans le titre, quel type de données un lexer doit-il retourner / donner à l'analyseur?

"Token", évidemment. Un lexer produit un flux de jetons, il doit donc renvoyer un flux de jetons .

Il a mentionné Flex, un lexer déjà existant, et a déclaré que l'écriture de «règles» avec lui serait plus simple que d'écrire un lexer à la main.

Les lexers générés par machine ont l'avantage que vous pouvez les générer rapidement, ce qui est particulièrement pratique si vous pensez que votre grammaire lexicale va beaucoup changer. Ils ont l'inconvénient que vous n'avez souvent pas beaucoup de flexibilité dans vos choix d'implémentation.

Cela dit, peu importe si c'est "plus simple"? Écrire le lexer n'est généralement pas la partie la plus difficile!

Lors de l'écriture d'un lexer et en supposant qu'il ne pourrait renvoyer qu'un seul type de données (chaînes ou nombres), quel serait le choix le plus logique?

Ni. Un lexer a généralement une "prochaine" opération qui renvoie un jeton, il doit donc retourner un jeton . Un jeton n'est pas une chaîne ou un nombre. C'est un signe.

Le dernier lexer que j'ai écrit était un lexer de «fidélité totale», ce qui signifie qu'il a renvoyé un jeton qui a suivi l'emplacement de tous les espaces et commentaires - que nous appelons «anecdotes» - dans le programme, ainsi que le jeton. Dans mon lexer, un jeton était défini comme:

  • Un éventail de triviaux de premier plan
  • Un genre symbolique
  • Une largeur de jeton en caractères
  • Un éventail de futilités finales

Trivia a été défini comme:

  • Un type de trivia - espaces, retour à la ligne, commentaire, etc.
  • Une largeur de trivia en caractères

Donc, si nous avions quelque chose comme

    foo + /* comment */
/* another comment */ bar;

qui LEX quatre jetons avec les types de jeton Identifier, Plus, Identifier, Semicolon, et les largeurs 3, 1, 3, 1. Le premier identifiant a conduit consistant en Anglais Whitespaceavec une largeur de 4 et de fuite Anglais Whitespaceavec une largeur de 1. Le Plusn'a pas conduit Anglais et trivia de fin composé d'un espace blanc, d'un commentaire et d'une nouvelle ligne. L'identifiant final a un trivial principal d'un commentaire et un espace, et ainsi de suite.

Avec ce schéma, chaque caractère du fichier est pris en compte dans la sortie du lexer, qui est une propriété pratique à avoir pour des choses comme la coloration de la syntaxe.

Bien sûr, si vous n'avez pas besoin de l'info, vous pouvez simplement créer un jeton deux choses: le type et la largeur.

Vous remarquerez peut-être que le jeton et les anecdotes ne contiennent que leur largeur, pas leur position absolue dans le code source. C'est délibéré. Un tel schéma présente des avantages:

  • Il est compact en format mémoire et filaire
  • Il permet de relexer les modifications; ceci est utile si le lexer s'exécute dans un IDE. Autrement dit, si vous détectez une modification dans un jeton, vous sauvegardez simplement votre lexer sur quelques jetons avant la modification et recommencez lexing jusqu'à ce que vous soyez synchronisé avec le flux de jetons précédent. Lorsque vous tapez un caractère, la position de chaque jeton après la modification de ce caractère, mais généralement un ou deux jetons seulement changent de largeur, vous pouvez donc réutiliser tout cet état.
  • Les décalages de caractères exacts de chaque jeton peuvent facilement être dérivés en itérant sur le flux de jetons et en gardant une trace du décalage actuel. Une fois que vous avez les décalages de caractères exacts, il est facile d'extraire le texte si nécessaire.

Si vous ne vous souciez d'aucun de ces scénarios, un jeton pourrait être représenté comme une sorte et un décalage, plutôt que comme une sorte et une largeur.

Mais le point clé à retenir est que la programmation est l'art de faire des abstractions utiles . Vous manipulez des jetons, faites donc une abstraction utile sur les jetons, puis vous pouvez choisir vous-même les détails d'implémentation qui le sous-tendent.

Eric Lippert
la source
3

Généralement, vous renvoyez une petite structure qui a un nombre signifiant le jeton (ou une valeur énumérée pour une facilité d'utilisation) et une valeur facultative (chaîne, ou éventuellement valeur générique / basée sur un modèle). Une autre approche serait de renvoyer un type dérivé pour les éléments qui doivent transporter des données supplémentaires. Les deux sont des solutions assez désagréables, mais assez fines à un problème pratique.

Telastyn
la source
Qu'entendez-vous par légèrement désagréable ? Sont-ils des moyens inefficaces d'obtenir des valeurs de chaîne?
Christian Dean
@ Mr.Python - ils mèneront à beaucoup de vérifications avant utilisation dans le code, ce qui est inefficace, mais moreso rend le code un peu plus complexe / fragile.
Telastyn
J'ai une question similaire lors de la conception d'un lexer en C ++, je pourrais retourner un Token *ou simplement un Token, ou un TokenPtrqui est un pointeur partagé de Tokenclasse. Mais je vois également un lexer renvoyer juste un TokenType et stocker la valeur de chaîne ou de nombre dans d'autres variables globales ou statiques. Une autre question est la suivante: comment pouvons-nous stocker les informations de localisation, dois-je avoir une structure Token contenant des champs TokenType, String et Location? Merci.
ollydbg23
@ ollydbg23 - n'importe laquelle de ces choses peut fonctionner. J'utiliserais une structure. Et pour les langues non apprenantes, vous utiliserez de toute façon un générateur d'analyseur.
Telastyn
@Telastyn merci pour la réponse. Vous voulez dire qu'une structure Token pourrait être quelque chose comme ça struct Token {TokenType id; std::string lexeme; int line; int column;}, non? Pour une fonction publique de Lexer, telle que PeekToken(), la fonction peut renvoyer un Token *ou TokenPtr. Je pense que pendant un certain temps, si la fonction renvoie simplement le TokenType, comment l'analyseur essaie-t-il d'obtenir les autres informations sur le Token? Donc, un pointeur comme le type de données est préférable pour le retour d'une telle fonction. Des commentaires sur mon idée? Merci
ollydbg23