Je travaille actuellement avec un programme Lexical Analyzer et j'utilise Java. Je cherchais des réponses à ce problème, mais jusqu'à présent, je n'en ai trouvé aucune. Voici mon problème:
Contribution:
System.out.println ("Hello World");
Sortie désirée:
Lexeme----------------------Token
System [Key_Word]
. [Object_Accessor]
out [Key_Word]
. [Object_Accessor]
println [Key_Word]
( [left_Parenthesis]
"Hello World" [String_Literal]
) [right_Parenthesis]
; [statement_separator]
Je suis toujours débutant et j'espère que vous pourrez m'aider à ce sujet. Merci.
ANTLR ni le livre Dragon ne sont nécessaires pour écrire un analyseur lexical simple à la main. Même les analyseurs lexicaux pour des langages plus complets (comme Java) ne sont pas terriblement compliqués à écrire à la main. Évidemment, si vous avez une tâche industrielle, vous voudrez peut-être envisager des outils industriels tels que ANTLR ou une variante de Lex, mais pour comprendre comment fonctionne l'analyse lexicale, écrire un à la main s'avérera probablement un exercice utile. Je suppose que c'est le cas, puisque vous avez dit que vous êtes toujours un débutant.
Voici un analyseur lexical simple, écrit en Java, pour un sous-ensemble d'un langage semblable à Scheme, que j'ai écrit après avoir vu cette question. Je pense que le code est relativement facile à comprendre même si vous n'avez jamais vu un lexer auparavant, tout simplement parce que briser un flux de caractères (dans ce cas un String
) en un flux de jetons (dans ce cas un List<Token>
) n'est-ce pas difficile. Si vous avez des questions, je peux essayer d’expliquer plus en profondeur.
import Java.util.List;
import Java.util.ArrayList;
/*
* Lexical analyzer for Scheme-like minilanguage:
* (define (foo x) (bar (baz x)))
*/
public class Lexer {
public static enum Type {
// This Scheme-like language has three token types:
// open parens, close parens, and an "atom" type
LPAREN, RPAREN, ATOM;
}
public static class Token {
public final Type t;
public final String c; // contents mainly for atom tokens
// could have column and line number fields too, for reporting errors later
public Token(Type t, String c) {
this.t = t;
this.c = c;
}
public String toString() {
if(t == Type.ATOM) {
return "ATOM<" + c + ">";
}
return t.toString();
}
}
/*
* Given a String, and an index, get the atom starting at that index
*/
public static String getAtom(String s, int i) {
int j = i;
for( ; j < s.length(); ) {
if(Character.isLetter(s.charAt(j))) {
j++;
} else {
return s.substring(i, j);
}
}
return s.substring(i, j);
}
public static List<Token> Lex(String input) {
List<Token> result = new ArrayList<Token>();
for(int i = 0; i < input.length(); ) {
switch(input.charAt(i)) {
case '(':
result.add(new Token(Type.LPAREN, "("));
i++;
break;
case ')':
result.add(new Token(Type.RPAREN, ")"));
i++;
break;
default:
if(Character.isWhitespace(input.charAt(i))) {
i++;
} else {
String atom = getAtom(input, i);
i += atom.length();
result.add(new Token(Type.ATOM, atom));
}
break;
}
}
return result;
}
public static void main(String[] args) {
if(args.length < 1) {
System.out.println("Usage: Java Lexer \"((some Scheme) (code to) Lex)\".");
return;
}
List<Token> tokens = Lex(args[0]);
for(Token t : tokens) {
System.out.println(t);
}
}
}
Exemple d'utilisation:
~/code/scratch $ Java Lexer ""
~/code/scratch $ Java Lexer "("
LPAREN
~/code/scratch $ Java Lexer "()"
LPAREN
RPAREN
~/code/scratch $ Java Lexer "(foo)"
LPAREN
ATOM<foo>
RPAREN
~/code/scratch $ Java Lexer "(foo bar)"
LPAREN
ATOM<foo>
ATOM<bar>
RPAREN
~/code/scratch $ Java Lexer "(foo (bar))"
LPAREN
ATOM<foo>
LPAREN
ATOM<bar>
RPAREN
RPAREN
Une fois que vous avez écrit un ou deux lexers simples comme celui-ci, vous aurez une assez bonne idée de la façon dont ce problème se décompose. Ensuite, il serait intéressant d’explorer comment utiliser des outils automatisés comme Lex. La théorie à la base des opérateurs d’expression régulière n’est pas trop difficile à comprendre, mais il faut un certain temps pour bien la comprendre. Je pense que l'écriture manuelle de lexers motive cette étude et vous aide à mieux comprendre le problème que de plonger dans la théorie qui sous-tend la conversion d'expressions régulières en automates finis (d'abord les NFA, puis les NFA en DFA), etc. beaucoup à prendre en même temps, et il est facile de se sentir dépassé.
Personnellement, bien que le livre Dragon soit bon et très complet, la couverture n’est peut-être pas la plus facile à comprendre car elle se veut complète, pas nécessairement accessible. Vous voudrez peut-être essayer d’autres textes du compilateur avant d’ouvrir le livre Dragon. Voici quelques livres gratuits, qui ont une bonne introduction, IMHO:
http://www.ethoberon.ethz.ch/WirthPubl/CBEAll.pdf
http://www.diku.dk/~torbenm/Basics/
Quelques articles sur l'implémentation d'expressions régulières (l'analyse lexicale automatisée utilise généralement des expressions régulières)
J'espère que ça aide. Bonne chance.
ANTLR 4 fera exactement cela avec la grammaire de référence Java.g4
. Vous avez le choix entre deux options, en fonction de la précision avec laquelle vous souhaitez que la gestion des séquences d'échappement Unicode suive les spécifications de langue.
ANTLRInputStream
dans un JavaUnicodeInputStream
, qui traite les séquences d'échappement Unicode conformément au JLS avant de les transmettre au lexer.Edit: Les noms des jetons produits par cette grammaire diffèrent légèrement de votre tableau.
Key_Word
est Identifier
Object_Accessor
est DOT
left_Parenthesis
est LPAREN
String_Literal
est StringLiteral
right_Parenthesis
est RPAREN
statement_separator
est SEMI
Vous pouvez utiliser des bibliothèques telles que Lex & Bison
en C ou Antlr
en Java. L'analyse lexicale peut être réalisée en faisant des automates. Je vais vous donner un petit exemple:
Supposons que vous deviez créer une chaîne où les mots-clés (langue) sont {'echo', '.', ' ', 'end')
. Par mots-clés, j'entends la langue, c'est-à-dire les mots-clés suivants. Donc si j'entre
echo .
end .
Mon lexer devrait sortir
echo ECHO
SPACE
. DOT
end END
SPACE
. DOT
Maintenant, pour construire des automates pour un tel tokenizer, je peux commencer par
->(SPACE) (Back)
|
(S)-------------E->C->H->O->(ECHO) (Back)
| |
.->(DOT)(Back) ->N->D ->(END) (Back to Start)
Le diagramme ci-dessus est très mauvais, mais l’idée est que vous avez un état de départ représenté par S
et que vous consommez maintenant E
et que vous passez à un autre état. Vous vous attendez maintenant à ce que N
ou C
arrive pour END
et ECHO
. Vous continuez à consommer des personnages et à atteindre différents états dans cette simple machine à états finis. En fin de compte, vous atteignez certains états Emit
, par exemple après avoir consommé E
, N
, D
, vous atteignez l'état d'émission pour END
qui émet le jeton puis vous revenez à l'état start
. Ce cycle se poursuit indéfiniment dans la mesure où des flux de caractères arrivent sur votre tokenizer. Sur un caractère invalide, vous pouvez soit renvoyer une erreur, soit ignorer, en fonction de la conception.
L'analyse lexicale est un sujet en soi qui va généralement de pair avec la conception et l'analyse du compilateur. Vous devriez lire à ce sujet avant d'essayer de coder quoi que ce soit. Mon livre préféré sur ce sujet est le livre Dragon qui devrait vous donner une bonne introduction à la conception du compilateur et même fournir des pseudocodes pour toutes les phases du compilateur que vous pouvez facilement traduire en Java et déplacer à partir de là.
En bref, l’idée principale est d’analyser l’entrée et de la diviser en jetons appartenant à certaines classes (parenthèses ou mots-clés, par exemple, dans la sortie souhaitée) à l’aide d’une machine à états finis. Le processus de construction de la machine à états est en réalité le seul élément difficile de cette analyse et le livre Dragon vous en donnera une bonne idée.
CookCC ( https://github.com/coconut2015/cookcc ) génère un lexer très rapide, petit et sans dépendance pour Java.