Lexikalische Analyse, Teil 2: Erstellen Sie eine Anwendung

Letzten Monat habe ich mir die Klassen angesehen, die Java für grundlegende lexikalische Analysen bereitstellt. Diesen Monat werde ich eine einfache Anwendung durchgehen, mit StreamTokenizerder ein interaktiver Taschenrechner implementiert wird.

Um den Artikel des letzten Monats kurz zu lesen, gibt es zwei Lexical-Analyzer-Klassen, die in der Standard-Java-Distribution enthalten sind: StringTokenizerund StreamTokenizer. Diese Analysatoren wandeln ihre Eingabe in diskrete Token um, mit denen ein Parser eine bestimmte Eingabe verstehen kann. Der Parser implementiert eine Grammatik, die als ein oder mehrere Zielzustände definiert ist, die durch das Anzeigen verschiedener Token-Sequenzen erreicht werden. Wenn der Zielstatus eines Parsers erreicht ist, führt er eine Aktion aus. Wenn der Parser feststellt, dass angesichts der aktuellen Token-Sequenz keine möglichen Zielzustände vorhanden sind, definiert er dies als Fehlerzustand. Wenn ein Parser einen Fehlerzustand erreicht, führt er eine Wiederherstellungsaktion aus, die den Parser an einen Punkt zurückbringt, an dem er erneut mit dem Parsen beginnen kann. In der Regel wird dies implementiert, indem Token verbraucht werden, bis der Parser wieder zu einem gültigen Startpunkt zurückgekehrt ist.

Letzten Monat habe ich Ihnen einige Methoden gezeigt, StringTokenizermit denen einige Eingabeparameter analysiert wurden. Diesen Monat zeige ich Ihnen eine Anwendung, die ein StreamTokenizerObjekt verwendet, um einen Eingabestream zu analysieren und einen interaktiven Taschenrechner zu implementieren.

Erstellen einer Anwendung

Unser Beispiel ist ein interaktiver Taschenrechner, der dem Befehl Unix bc (1) ähnelt. Wie Sie sehen werden, wird die StreamTokenizerKlasse als lexikalischer Analysator an den Rand ihres Dienstprogramms gedrängt. Somit ist es eine gute Demonstration, wo die Grenze zwischen "einfachen" und "komplexen" Analysatoren gezogen werden kann. Dieses Beispiel ist eine Java-Anwendung und wird daher am besten über die Befehlszeile ausgeführt.

Als kurze Zusammenfassung seiner Fähigkeiten akzeptiert der Rechner Ausdrücke im Formular

[Variablenname] "=" Ausdruck 

Der Variablenname ist optional und kann eine beliebige Zeichenfolge im Standardwortbereich sein. (Sie können das Übungs-Applet aus dem Artikel des letzten Monats verwenden, um Ihr Gedächtnis für diese Zeichen zu aktualisieren.) Wenn der Variablenname weggelassen wird, wird der Wert des Ausdrucks einfach gedruckt. Wenn der Variablenname vorhanden ist, wird der Wert des Ausdrucks der Variablen zugewiesen. Sobald Variablen zugewiesen wurden, können sie in späteren Ausdrücken verwendet werden. Somit erfüllen sie die Rolle von "Erinnerungen" auf einem modernen Taschenrechner.

Der Ausdruck besteht aus Operanden in Form von numerischen Konstanten (doppelte Genauigkeit, Gleitkommakonstanten) oder Variablennamen, Operatoren und Klammern zum Gruppieren bestimmter Berechnungen. Die zulässigen Operatoren sind Addition (+), Subtraktion (-), Multiplikation (*), Division (/), bitweises UND (&), bitweises ODER (|), bitweises XOR (#), Exponentiation (^) und unäre Negation entweder mit minus (-) für das Zweierkomplement-Ergebnis oder mit Knall (!) für das Zweierkomplement-Ergebnis.

Zusätzlich zu diesen Anweisungen kann unsere Taschenrechneranwendung einen von vier Befehlen ausführen: "dump", "clear", "help" und "quit". Der dumpBefehl druckt alle aktuell definierten Variablen sowie deren Werte aus. Der clearBefehl löscht alle aktuell definierten Variablen. Der helpBefehl druckt einige Zeilen Hilfetext aus, um den Benutzer zum Starten zu bewegen. Der quitBefehl bewirkt, dass die Anwendung beendet wird.

Die gesamte Beispielanwendung besteht aus zwei Parsern - einem für Befehle und Anweisungen und einem für Ausdrücke.

Erstellen eines Befehlsparsers

Der Befehlsparser ist in der Anwendungsklasse für das Beispiel STExample.java implementiert. (Einen Zeiger auf den Code finden Sie im Abschnitt Ressourcen.) Die mainMethode für diese Klasse ist unten definiert. Ich werde für dich durch die Stücke gehen.

1 public static void main (String args []) löst IOException aus {2 Hashtable-Variablen = new Hashtable (); 3 StreamTokenizer st = neuer StreamTokenizer (System.in); 4 st.eolIsSignificant (true); 5 st.lowerCaseMode (true); 6 st.ordinaryChar ('/'); 7. st.ordinaryChar ('-');

Im obigen Code ordne ich als erstes eine java.util.HashtableKlasse zu, die die Variablen enthält. Danach ordne ich a zu StreamTokenizerund passe es leicht von seinen Standardeinstellungen an. Die Gründe für die Änderungen lauten wie folgt:

  • eolIsSignificant wird auf true gesetzt, damit der Tokenizer eine Anzeige des Zeilenendees zurückgibt . Ich benutze das Ende der Linie als den Punkt, an dem der Ausdruck endet.

  • lowerCaseMode wird auf true gesetzt, damit die Variablennamen immer in Kleinbuchstaben zurückgegeben werden. Auf diese Weise wird bei Variablennamen nicht zwischen Groß- und Kleinschreibung unterschieden.

  • Das Schrägstrichzeichen (/) ist ein normales Zeichen, sodass es nicht zum Anzeigen des Beginns eines Kommentars verwendet wird und stattdessen als Divisionsoperator verwendet werden kann.

  • Das Minuszeichen (-) ist ein gewöhnliches Zeichen, sodass die Zeichenfolge "3-3" in drei Token unterteilt wird - "3", "-" und "3" - und nicht nur "3" und "-3." (Denken Sie daran, dass das Parsen von Zahlen standardmäßig auf "Ein" gesetzt ist.)

Sobald der Tokenizer eingerichtet ist, wird der Befehlsparser in einer Endlosschleife ausgeführt (bis er den Befehl "quit" erkennt, an welchem ​​Punkt er beendet wird). Dies ist unten gezeigt.

8 while (true) {9 Ausdruck res; 10 int c = StreamTokenizer.TT_EOL; 11 String varName = null; 12 13 System.out.println ("Geben Sie einen Ausdruck ein ..."); 14 try {15 while (true) {16 c = st.nextToken (); 17 if (c == StreamTokenizer.TT_EOF) {18 System.exit (1); 19} else if (c == StreamTokenizer.TT_EOL) {20 continue; 21} else if (c == StreamTokenizer.TT_WORD) {22 if (st.sval.compareTo ("dump") == 0) {23 dumpVariables (Variablen); 24 weiter; 25} else if (st.sval.compareTo ("clear") == 0) {26 variables = new Hashtable (); 27 weiter; 28} else if (st.sval.compareTo ("quit") == 0) {29 System.exit (0); 30} else if (st.sval.compareTo ("exit") == 0) {31 System.exit (0); 32} else if (st.sval.compareTo ("help") == 0) {33 help (); 34 weiter; 35} 36 varName = st.sval; 37 c = st.nextToken ();38} 39 Pause; 40} 41 if (c! = '=') {42 throw new SyntaxError ("fehlendes Anfangszeichen '=' sign."); 43}

Wie Sie in Zeile 16 sehen können, wird das erste Token durch Aufrufen nextTokendes StreamTokenizerObjekts aufgerufen . Dies gibt einen Wert zurück, der die Art des gescannten Tokens angibt. Der Rückgabewert ist entweder eine der definierten Konstanten in der StreamTokenizerKlasse oder ein Zeichenwert. Die "Meta" -Token (diejenigen, die nicht einfach Zeichenwerte sind) sind wie folgt definiert:

  • TT_EOF- Dies zeigt an, dass Sie sich am Ende des Eingabestreams befinden. Im Gegensatz dazu StringTokenizergibt es keine hasMoreTokensMethode.

  • TT_EOL - Dies zeigt an, dass das Objekt gerade eine Zeilenende-Sequenz passiert hat.

  • TT_NUMBER - Dieser Token-Typ teilt Ihrem Parser-Code mit, dass auf der Eingabe eine Nummer angezeigt wurde.

  • TT_WORD - Dieser Token-Typ zeigt an, dass ein ganzes "Wort" gescannt wurde.

Wenn das Ergebnis keine der oben genannten Konstanten ist, ist es entweder der Zeichenwert, der ein Zeichen im "normalen" Zeichenbereich darstellt, der gescannt wurde, oder eines der von Ihnen festgelegten Anführungszeichen. (In meinem Fall ist kein Anführungszeichen festgelegt.) Wenn das Ergebnis eines Ihrer Anführungszeichen ist, befindet sich die Zeichenfolge in Anführungszeichen in der Zeichenfolgeninstanzvariablen svaldes StreamTokenizerObjekts.

Der Code in den Zeilen 17 bis 20 behandelt Zeilen- und Dateiende-Anzeigen, während in Zeile 21 die if-Klausel verwendet wird, wenn ein Wort-Token zurückgegeben wurde. In diesem einfachen Beispiel ist das Wort entweder ein Befehl oder ein Variablenname. Die Zeilen 22 bis 35 behandeln die vier möglichen Befehle. Wenn Zeile 36 erreicht ist, muss es sich um einen Variablennamen handeln. Folglich behält das Programm eine Kopie des Variablennamens und erhält das nächste Token, das ein Gleichheitszeichen sein muss.

Wenn in Zeile 41 das Token kein Gleichheitszeichen war, erkennt unser einfacher Parser einen Fehlerzustand und löst eine Ausnahme aus, um ihn zu signalisieren. Ich habe zwei generische Ausnahmen erstellt SyntaxErrorund ExecError, um Analysezeitfehler von Laufzeitfehlern zu unterscheiden. Das mainVerfahren wird mit Zeile 44 fortgesetzt.

44 res = ParseExpression.expression (st); 45} catch (SyntaxError se) {46 res = null; 47 varName = null; 48 System.out.println ("\ nSyntaxfehler erkannt! -" + se.getMsg ()); 49 while (c! = StreamTokenizer.TT_EOL) 50 c = st.nextToken (); 51 weiter; 52}

In Zeile 44 wird der Ausdruck rechts vom Gleichheitszeichen mit dem in der ParseExpressionKlasse definierten Ausdrucksparser analysiert . Beachten Sie, dass die Zeilen 14 bis 44 in einen Try / Catch-Block eingeschlossen sind, der Syntaxfehler abfängt und diese behandelt. Wenn ein Fehler erkannt wird, verbraucht der Parser alle Token bis einschließlich des nächsten Zeilenende-Tokens. Dies ist in den Zeilen 49 und 50 oben gezeigt.

At this point, if an exception was not thrown, the application has successfully parsed a statement. The final check is to see that the next token is the end of line. If it isn't, an error has gone undetected. The most common error will be mismatched parentheses. This check is shown in lines 53 through 60 of the code below.

53 c = st.nextToken(); 54 if (c != StreamTokenizer.TT_EOL) { 55 if (c == ')') 56 System.out.println("\nSyntax Error detected! - To many closing parens."); 57 else 58 System.out.println("\nBogus token on input - "+c); 59 while (c != StreamTokenizer.TT_EOL) 60 c = st.nextToken(); 61 } else { 

When the next token is an end of line, the program executes lines 62 through 69 (shown below). This section of the method evaluates the parsed expression. If the variable name was set in line 36, the result is stored in the symbol table. In either case, if no exception is thrown, the expression and its value are printed to the System.out stream so that you can see what the parser decoded.

62 try { 63 Double z; 64 System.out.println("Parsed expression : "+res.unparse()); 65 z = new Double(res.value(variables)); 66 System.out.println("Value is : "+z); 67 if (varName != null) { 68 variables.put(varName, z); 69 System.out.println("Assigned to : "+varName); 70 } 71 } catch (ExecError ee) { 72 System.out.println("Execution error, "+ee.getMsg()+"!"); 73 } 74 } 75 } 76 } 

In the STExample class, the StreamTokenizer is being used by a command-processor parser. This type of parser commonly would be used in a shell program or in any situation in which the user issues commands interactively. The second parser is encapsulated in the ParseExpression class. (See the Resources section for the complete source.) This class parses the calculator's expressions and is invoked in line 44 above. It is here that StreamTokenizer faces its stiffest challenge.

Building an expression parser

The grammar for the calculator's expressions defines an algebraic syntax of the form "[item] operator [item]." This type of grammar comes up again and again and is called an operator grammar. A convenient notation for an operator grammar is:

id ( "OPERATOR" id )* 

The code above would be read "An ID terminal followed by zero or more occurrences of an operator-id tuple." The StreamTokenizer class would seem pretty ideal for analyzing such streams, because the design naturally breaks the input stream into word, number, and ordinary character tokens. As I'll show you, this is true up to a point.

The ParseExpression class is a straightforward, recursive-descent parser for expressions, right out of an undergraduate compiler-design class. The Expression method in this class is defined as follows:

1 statischer Ausdruck Ausdruck (StreamTokenizer st) löst SyntaxError aus {2 Ausdruck Ergebnis; 3 boolean done = false; 4 5 Ergebnis = Summe (st); 6 while (! Fertig) {7 try {8 switch (st.nextToken ()) 9 case '&': 10 result = neuer Ausdruck (OP_AND, result, sum (st)); 11 Pause; 12 case '23} catch (IOException ioe) {24 throw new SyntaxError ("Ich habe eine E / A-Ausnahme."); 25} 26} 27 Ergebnis zurückgeben; 28}