Erstellen Sie einen Interpreter in Java – Implementieren Sie die Ausführungsengine

Für diejenigen unter Ihnen, die gerade zu uns gekommen sind, habe ich in meiner Spalte „Java In Depth“ in den letzten Monaten diskutiert, wie man einen Interpreter in Java erstellen könnte. In der ersten Interpreter-Spalte haben wir einige der wünschenswerten Attribute eines Interpreters behandelt; In der zweiten Spalte haben wir sowohl das Parsen als auch das Layout eines Klassenpakets zur Implementierung des Interpreters besprochen. In dieser Spalte werden wir uns die Ausführung des Interpreters und die dafür erforderlichen Unterstützungsklassen ansehen. Schließlich werde ich die Serie hier mit einer Diskussion darüber abschließen, wie ein Interpreter in andere Java-Klassen eingebunden werden kann, um so ihre Fähigkeiten zu verbessern.

Überprüfung der relevanten Bits

Lassen Sie mich zunächst skizzieren, was wir bisher behandelt haben, und auf die Teile des Designs hinweisen, die bei der Erörterung des Ausführungsmodus wichtiger werden. Eine detailliertere Beschreibung dieser Klassen finden Sie in meinen vorherigen Spalten oder in den Quellcode-Links im Abschnitt

Ressourcen

unten.

Es gibt drei Grundlagenklassen in der Implementierung des Interpreters, Program, Statement und Expression. Das Folgende zeigt, wie die drei verwandt sind:

Programm

Diese Program Klasse klebt die Analyse- und Ausführungskomponenten des Parsers zusammen. Diese Klasse definiert zwei Hauptmethoden, load und run. Die load -Methode liest Anweisungen aus einem Eingabestream und analysiert sie in eine Sammlung von Anweisungen, die run -Methode iteriert über die Sammlung und führt jede der Anweisungen aus. Die Program -Klasse bietet auch eine Sammlung von Variablen für das Programm sowie einen Stapel zum Speichern von Daten.

Anweisung

Die Statement -Klasse enthält eine einzelne analysierte Anweisung. Diese Klasse ist tatsächlich in einen bestimmten Anweisungstyp unterteilt (PRINT , GOTO, IF usw.), aber alle Anweisungen enthalten die Methode execute, die aufgerufen wird, um die Anweisung im Kontext einer Program Klasseninstanz auszuführen.

Expression

Die Expression-Klasse enthält den Analysebaum eines Ausdrucks. Während der Ausführung wird die Methode value verwendet, um den Ausdruck auszuwerten und seinen Wert zurückzugeben. Wie Statement ist die Klasse Expression in erster Linie für die Unterklasse bestimmter Ausdruckstypen ausgelegt.

Alle diese Klassen bilden zusammen die Basis eines Interpreters. Die Klasse Program kapselt gleichzeitig die Analyseoperation und die Ausführungsoperation, während die Klassen Statement und Expression die tatsächlichen Berechnungskonzepte der von uns implementierten Sprache kapseln. Für diese drei Artikel zum Erstellen von Dolmetschern war die Beispielsprache GRUNDLEGEND.

Berechnungsmöglichkeiten

Der Interpreter enthält zwei ausführbare Klassen,

Statement

und

Expression

. Schauen wir uns zuerst an

Expression

Die Instanzen von Expression werden mit der Methode expression in der Klasse ParseExpression erstellt. Die Klasse ParseExpression implementiert den Ausdrucksparser in diesem Interpreter. Diese Klasse ist ein Peer der Klasse ParseStatement, die die Methode statement verwendet, um GRUNDLEGENDE Anweisungen zu analysieren. Instanzen von Expression haben ein internes type , das angibt, welchen Operator die Instanz darstellt, und zwei Methoden, value und stringValue, die den berechneten Wert des Ausdrucks zurückgeben. Wenn eine Ausdrucksinstanz erstellt wird, erhält sie nominell zwei Parameter, die die linke und rechte Seite der Operation des Ausdrucks darstellen. In Quellform dargestellt, lautet der erste Teil des Ausdrucks wie folgt:

class Expression { Expression arg1, arg2; int oper; final static int OP_ADD = 1; // Addition '+' final static int OP_SUB = 2; // Subtraction '-' final static int OP_MUL = 3; // Multiplication '*' final static int OP_DIV = 4; // Division '/' final static int OP_BNOT = 19; // Boolean negation '.NOT.' final static int OP_NEG = 20; // Unary minus

Wie Sie im obigen Code sehen können, gibt es die Instanzvariablen, einen Operatortyp namens oper und zwei Hälften der Operation in arg1 und arg2 sowie einige Konstanten zum Definieren der verschiedenen Typen. Außerdem gibt es zwei Konstruktoren, die vom Ausdrucksparser verwendet werden; Diese erstellen einen neuen Ausdruck aus zwei Ausdrücken. Ihre Quelle ist unten gezeigt:

protected Expression(int op, Expression a, Expression b) throws BASICSyntaxError { arg1 = a; arg2 = b; oper = op; /* * If the operator is a boolean, both arguments must be boolean. */ if (op > OP_GE) { if ( (! (arg1 instanceof BooleanExpression)) || (! (arg2 instanceof BooleanExpression)) ) throw new BASICSyntaxError(typeError); } else { if ((arg1 instanceof BooleanExpression) || (arg2 instanceof BooleanExpression)) throw new BASICSyntaxError(typeError); } } protected Expression(int op, Expression a) throws BASICSyntaxError { arg2 = a; oper = op; if ((oper == OP_BNOT) && (! (arg2 instanceof BooleanExpression))) throw new BASICSyntaxError(typeError); }

Der erste Konstruktor erstellt ein beliebiges Ausdrucksobjekt und der zweite ein „unäres“ Ausdrucksobjekt – z. B. unary minus . Eine Sache zu beachten ist, dass, wenn es nur ein Argument gibt, arg2 verwendet wird, um seinen Wert zu speichern.

Die Methode, die in der Klasse Expression am häufigsten verwendet wird, ist value, die wie folgt definiert ist:

 double value(Program pgm) throws BASICRuntimeError { switch (oper) { case OP_ADD : return arg1.value(pgm) + arg2.value(pgm); case OP_SUB : return arg1.value(pgm) - arg2.value(pgm); ... etc for all of the other operator types. ...

Sie können sehen, dass jedes Ausdrucksobjekt ein Tupel darstellt, das aus einem Operator und einem oder zwei Argumenten besteht. Der lustige Teil des Entwurfs der Ausdrucksausführungs-Engine auf diese Weise besteht darin, dass Sie beim Erstellen dieser Gruppe von Ausdruckstupeln basierend auf dem Expression -Objekt den Wert des Ausdrucks berechnen können, indem Sie einfach die value -Methode aufrufen. Die value -Methode ruft rekursiv die value -Methode der beiden Argumente auf, aus denen dieser Ausdruck besteht, wendet die Operation auf sie an und gibt das Ergebnis zurück. Dieses Design wurde verwendet, damit Ausdrücke leicht zu verstehen sind.

Um die Klassenstruktur sauber zu halten, sind alle Recheneinheiten – von Konstanten bis zu trigonometrischen Funktionen – Unterklassen von Expression . Diese Idee, die schamlos aus Lisp gestohlen wurde, kapselt den Begriff des „Verursachens“ einer Bewertung vollständig von der tatsächlichen Implementierung des „Auftretens“ dieser Bewertung ab. Um zu demonstrieren, wie dieses Prinzip angewendet wird, müssen wir nur einige der spezialisierten Unterklassen von Expression betrachten.

Konstanten in meiner Version von BASIC, die ich COCOA genannt habe, werden durch die Klasse ConstantExpression dargestellt, die Expression unterordnet und den numerischen Wert einfach in einem Elementwert speichert. Der Quellcode zu ConstantExpression ist unten konzeptionell dargestellt. Ich sage „konzeptionell“, weil ich beschlossen habe, das, was StringConstantExpression und NumericConstantExpression gewesen wäre, in einer einzigen Klasse zu bündeln. Die Real-Klasse enthält also einen Konstruktor zum Erstellen einer Konstanten mit einem String-Argument und zum Zurückgeben ihres Werts als String. Der folgende Code zeigt, wie die Klasse ConstantExpression numerische Konstanten behandelt.

class ConstantExpression extends Expression { private double v; ConstantExpression(double a) { super(); v = a; } double value(Program pgm) throws BASICRuntimeError { return v; }}

Der oben gezeigte Code ersetzt die komplizierteren Konstruktoren von Expression durch eine einfache Speicherung einer Instanzvariablen; Die value -Methode wird einfach durch eine Rückgabe des gespeicherten Werts ersetzt.

Es ist wahr, dass Sie die Expression -Klasse so codieren könnten, dass sie in ihren Konstruktoren eine Konstante akzeptiert, die Ihnen eine Klasse erspart. Ein Vorteil des Designs von Expression ist jedoch, dass der Code in Expression maximal generisch bleibt. Wenn ich mit dem Expression -Code „fertig“ bin, kann ich zu anderen Aspekten von Ausdrücken übergehen, ohne die Basisklasse immer wieder zu besuchen. Der Vorteil wird klarer, wenn wir in eine andere Unterklasse von Expression mit dem Namen FunctionExpression eintauchen.

In der Klasse FunctionExpression gab es zwei Designanforderungen, die meiner Meinung nach erfüllt werden sollten, um den Interpreter flexibel zu halten. Die erste bestand darin, die Standardgrundfunktionen zu implementieren; die andere bestand darin, das Parsen der Funktionsargumente in dieselbe Klasse zu kapseln, die diese Funktionen implementiert hat. Die zweite Anforderung, das Parsen, wurde durch den Wunsch motiviert, dieses BASIC erweiterbar zu machen, indem zusätzliche Funktionsbibliotheken erstellt wurden, die als Unterklassen von FunctionExpression an den Parser übergeben werden konnten. Ferner könnten diese übergebenen Klassen vom Parser verwendet werden, um die Anzahl der Funktionen zu erhöhen, die dem Programm des Benutzers zur Verfügung stehen.

Die FunctionExpression -Klasse ist nur mäßig komplizierter als eine ConstantExpression und wird unten in komprimierter Form dargestellt:

 1 class FunctionExpression extends Expression { 2 3 double value(Program p) throws BASICRuntimeError { 4 try { 5 switch (oper) { 6 case RND : 7 if (r == null) 8 r = p.getRandom(); 9 return (r.nextDouble() * arg2.value(p));10 case INT :11 return Math.floor(arg2.value(p));12 case SIN :13 return Math.sin(arg2.value(p));14 15 default :16 throw new BASICRuntimeError("Unknown or non-numeric function.");17 }18 } catch (Exception e) {19 if (e instanceof BASICRuntimeError)20 throw (BASICRuntimeError) e;21 else22 throw new BASICRuntimeError("Arithmetic Exception.");23 }24 }

Die obige Quelle zeigt, wie die value -Methode implementiert ist. Die oper-Variable wird wiederverwendet, um die Funktionsidentität zu speichern, und die von arg1 und arg2 referenzierten Ausdrucksobjekte werden als Argumente für die Funktionen selbst verwendet. Schließlich gibt es eine große switch-Anweisung, die die Anforderung sendet. Ein interessanter Aspekt ist, dass die value -Methode die potenziellen arithmetischen Ausnahmen abfängt und sie in Instanzen von BASICRuntimeError konvertiert. Der Parsing-Code in FunctionExpression wird unten angezeigt, um Platz zu sparen. (Denken Sie daran, dass der gesamte Quellcode über Links im Abschnitt Ressourcen verfügbar ist.)

 1 static FunctionExpression parse(int ty, LexicalTokenizer lt) throws BASICSyntaxError { 2 FunctionExpression result; 3 Expression a; 4 Expression b; 5 Expression se; 6 Token t; 7 8 t = lt.nextToken(); 9 if (! t.isSymbol('(')) {10 if (ty == RND) {11 lt.unGetToken();12 return new FunctionExpression(ty, new ConstantExpression(1));13 } else if (ty == FRE) {14 lt.unGetToken();15 return new FunctionExpression(ty, new ConstantExpression(0));16 }17 throw new BASICSyntaxError("Missing argument for function.");18 }19 switch (ty) {20 case RND:21 case INT:22 case SIN:23 case COS:24 case TAN:25 case ATN:26 case SQR:27 case ABS:28 case CHR:29 case VAL:30 case STR:31 case SPC:32 case TAB:33 case LOG:34 a = ParseExpression.expression(lt);35 if (a instanceof BooleanExpression) {36 throw new BASICSyntaxError(functions.toUpperCase()+" function cannot accept boolean expression.");37 }38 if ((ty == VAL) && (! a.isString()))39 throw new BASICSyntaxError(functions.toUpperCase()+" requires a string valued argument.");40 result = new FunctionExpression(ty, a);41 break; 42 default:43 throw new BASICSyntaxError("Unknown function on input.");4445 }46 t = lt.nextToken();47 if (! t.isSymbol(')')) {48 throw new BASICSyntaxError("Missing closing parenthesis for function.");49 }50 return result;51 }

Beachten Sie, dass dieser Code die Tatsache ausnutzt, dass der Ausdrucksparser in ParseStatement bereits herausgefunden hat, dass er einen Ausdruck betrachtet, und die Identität des Ausdrucks als Parameter ty übergeben hat. Dieser Parser muss dann nur die öffnende Klammer und die schließende Klammer finden, die die Argumente enthalten. Aber schauen Sie genau hin: In den Zeilen # 9 bis # 18 erlaubt der Parser einigen Funktionen, keine Argumente zu haben (in diesem Fall RND und FRE). Dies zeigt die Flexibilität, die der in diese Klasse integrierte Funktionsunterparser bietet, anstatt alle Funktionen zu zwingen, einer vordefinierten Vorlage zu entsprechen. Bei Angabe eines Funktionstyps im Parameter ty wählt die switch-Anweisung einen Zweig aus, der die für diese Funktion erforderlichen Argumente analysieren kann, seien es Zeichenfolgen, Zahlen, andere Ausdrücke usw.

Andere Aspekte: Strings und Arrays

Zwei weitere Teile der Basissprache werden vom COCOA-Interpreter implementiert: Strings und Arrays. Schauen wir uns zuerst die Implementierung von Strings an.

Um Zeichenfolgen als Variablen zu implementieren, wurde die Klasse Expression geändert, um den Begriff „Zeichenfolgenausdrücke“ aufzunehmen. Diese Änderung erfolgte in Form von zwei Ergänzungen: isString und stringValue. Die Quelle für diese beiden neuen Methoden ist unten dargestellt.

 String stringValue(Program pgm) throws BASICRuntimeError { throw new BASICRuntimeError("No String representation for this."); } boolean isString() { return false; }

Offensichtlich ist es für ein Basisprogramm nicht allzu nützlich, den Zeichenfolgenwert eines Basisausdrucks abzurufen (der immer entweder ein numerischer oder ein boolescher Ausdruck ist). Sie könnten aus dem Mangel an Nützlichkeit schließen, dass diese Methoden dann nicht in Expression und stattdessen in eine Unterklasse von Expression gehörten. Wenn Sie diese beiden Methoden jedoch in die Basisklasse einfügen, können alle Expression -Objekte getestet werden, um festzustellen, ob es sich tatsächlich um Zeichenfolgen handelt.

Ein anderer Entwurfsansatz besteht darin, die numerischen Werte als Zeichenfolgen mithilfe eines StringBuffer Objekts zurückzugeben, um einen Wert zu generieren. So könnte beispielsweise derselbe Code wie folgt umgeschrieben werden:

 String stringValue(Program pgm) throws BASICRuntimeError { StringBuffer sb = new StringBuffer(); sb.append(this.value(pgm)); return sb.toString(); }

Wenn der obige Code verwendet wird, können Sie die Verwendung von isString da jeder Ausdruck einen Zeichenfolgenwert zurückgeben kann. Darüber hinaus können Sie die value -Methode ändern, um zu versuchen, eine Zahl zurückzugeben, wenn der Ausdruck zu einer Zeichenfolge ausgewertet wird, indem Sie sie über die valueOf -Methode von java.lang.Double ausführen. In vielen Sprachen wie Perl, TCL und REXX wird diese Art der amorphen Typisierung mit großem Vorteil verwendet. Beide Ansätze sind gültig, und Sie sollten Ihre Wahl basierend auf dem Design Ihres Interpreters treffen. In BASIC muss der Interpreter einen Fehler zurückgeben, wenn einer numerischen Variablen eine Zeichenfolge zugewiesen wird.

Für Arrays gibt es verschiedene Möglichkeiten, wie Sie Ihre Sprache entwerfen können, um sie zu interpretieren. C verwendet die eckigen Klammern um Array-Elemente, um die Indexreferenzen des Arrays von Funktionsreferenzen zu unterscheiden, deren Argumente in Klammern stehen. Wenn also der Text NAME(V1, V2) vom Parser gesehen wird, kann es sich entweder um einen Funktionsaufruf oder eine Array-Referenz handeln.

Der lexikalische Analysator unterscheidet zwischen Token, denen Klammern folgen, indem er zuerst annimmt, dass es sich um Funktionen handelt, und darauf testet. Dann wird geprüft, ob es sich um Schlüsselwörter oder Variablen handelt. Diese Entscheidung verhindert, dass Ihr Programm eine Variable mit dem Namen „SIN.“ Jede Variable, deren Name mit einem Funktionsnamen übereinstimmt, wird vom lexikalischen Analysator stattdessen als Funktionstoken zurückgegeben. Der zweite Trick, den der lexikalische Analysator verwendet, besteht darin, zu überprüfen, ob auf den Variablennamen unmittelbar `(‚ folgt. Wenn dies der Fall ist, nimmt der Analysator an, dass es sich um eine Array-Referenz handelt. Indem wir dies im lexikalischen Analysator analysieren, verhindern wir, dass die Zeichenfolge `MYARRAY ( 2 )‚ als gültiges Array interpretiert wird (beachten Sie das Leerzeichen zwischen dem Variablennamen und der offenen Klammer).

Der letzte Trick zur Implementierung von Arrays liegt in der Klasse Variable . Diese Klasse wird für eine Instanz einer Variablen verwendet und ist, wie in der Spalte des letzten Monats erläutert, eine Unterklasse von Token . Es hat jedoch auch einige Maschinen, um Arrays zu unterstützen, und das werde ich unten zeigen:

class Variable extends Token { // Legal variable sub types final static int NUMBER = 0; final static int STRING = 1; final static int NUMBER_ARRAY = 2; final static int STRING_ARRAY = 4; String name; int subType; /* * If the variable is in the symbol table these values are * initialized. */ int ndx; // array indices. int mult; // array multipliers double nArrayValues; String sArrayValues;

Der obige Code zeigt die Instanzvariablen, die einer Variablen zugeordnet sind, wie in der Klasse ConstantExpression. Man muss eine Wahl über die Anzahl der zu verwendenden Klassen im Vergleich zur Komplexität einer Klasse treffen. Eine Entwurfsoption könnte darin bestehen, eine Variable -Klasse zu erstellen, die nur skalare Variablen enthält, und dann eine ArrayVariable -Unterklasse hinzuzufügen, um die Feinheiten von Arrays zu behandeln. Ich habe mich entschieden, sie zu kombinieren und skalare Variablen im Wesentlichen in Arrays der Länge 1 umzuwandeln.

Wenn Sie den obigen Code lesen, werden Array-Indizes und Multiplikatoren angezeigt. Diese sind hier, weil mehrdimensionale Arrays in BASIC mit einem einzigen linearen Java-Array implementiert werden. Der lineare Index in das Java-Array wird manuell mit den Elementen des Multiplikator-Arrays berechnet. Die im Basisprogramm verwendeten Indizes werden auf ihre Gültigkeit überprüft, indem sie mit dem maximal zulässigen Index im ndx-Array der Indizes verglichen werden.

Bei einem Basisarray mit den drei Dimensionen 10, 10 und 8 werden beispielsweise die Werte 10, 10 und 8 in ndx gespeichert. Auf diese Weise kann der Ausdrucksauswerter auf eine Bedingung „Index außerhalb der Grenzen“ testen, indem er die im Basisprogramm verwendete Zahl mit der maximalen zulässigen Zahl vergleicht, die jetzt in ndx gespeichert ist. Das Multiplikator-Array in unserem Beispiel würde die Werte 1, 10 und 100 enthalten. Diese Konstanten stellen die Zahlen dar, die man verwendet, um von einer mehrdimensionalen Array-Indexspezifikation in eine lineare Array-Indexspezifikation abzubilden. Die tatsächliche Gleichung lautet:

Java-Index = Index1 + Index2 * Maximale Größe von Index1 + Index3 * (MaxSize von Index1 * MaxSizeIndex 2)

Das nächste Java-Array in der Klasse Variable ist unten dargestellt.

 Expression expns;

Das expns-Array wird verwendet, um mit Arrays umzugehen, die als „A(10*B, i) .“ In diesem Fall sind die Indizes tatsächlich Ausdrücke und keine Konstanten, daher muss die Referenz Zeiger auf die Ausdrücke enthalten, die zur Laufzeit ausgewertet werden. Schließlich gibt es diesen ziemlich hässlich aussehenden Code, der den Index abhängig davon berechnet, was im Programm übergeben wurde. Diese private Methode wird unten gezeigt.

 private int computeIndex(int ii) throws BASICRuntimeError { int offset = 0; if ((ndx == null) || (ii.length != ndx.length)) throw new BASICRuntimeError("Wrong number of indices."); for (int i = 0; i < ndx.length; i++) { if ((ii < 1) || (ii > ndx)) throw new BASICRuntimeError("Index out of range."); offset = offset + (ii-1) * mult; } return offset; }

Wenn Sie sich den obigen Code ansehen, werden Sie feststellen, dass der Code zuerst überprüft, ob die richtige Anzahl von Indizes beim Verweisen auf das Array verwendet wurde, und dann, dass jeder Index innerhalb des zulässigen Bereichs für diesen Index lag. Wenn ein Fehler erkannt wird, wird eine Ausnahme an den Interpreter ausgelöst. Die Methoden numValue und stringValue geben einen Wert aus der Variablen als Zahl bzw. Diese beiden Methoden werden unten gezeigt.

 double numValue(int ii) throws BASICRuntimeError { return nArrayValues; } String stringValue(int ii) throws BASICRuntimeError { if (subType == NUMBER_ARRAY) return ""+nArrayValues; return sArrayValues; }

Es gibt zusätzliche Methoden zum Festlegen des Werts einer Variablen, die hier nicht angezeigt werden.

Indem ein Großteil der Komplexität der Implementierung jedes Teils verborgen wird, ist der Java-Code recht einfach, wenn es endlich an der Zeit ist, das Basisprogramm auszuführen.

Ausführen des Codes

Der Code, um die GRUNDLEGENDEN Anweisungen zu interpretieren und auszuführen, ist in der

run

methode der

Program

klasse. Der Code für diese Methode ist unten gezeigt, und ich werde es durchgehen, um auf die interessanten Teile hinzuweisen.

 1 public void run(InputStream in, OutputStream out) throws BASICRuntimeError { 2 PrintStream pout; 3 Enumeration e = stmts.elements(); 4 stmtStack = new Stack(); // assume no stacked statements ... 5 dataStore = new Vector(); // ... and no data to be read. 6 dataPtr = 0; 7 Statement s; 8 9 vars = new RedBlackTree();1011 // if the program isn't yet valid.12 if (! e.hasMoreElements())13 return;1415 if (out instanceof PrintStream) {16 pout = (PrintStream) out;17 } else {18 pout = new PrintStream(out);19 }

Der obige Code zeigt, dass die run -Methode einen InputStream und einen OutputStream als „Konsole“ für das ausführende Programm verwendet. In Zeile 3 wird das Enumerationsobjekt e auf den Satz von Anweisungen aus der Sammlung mit dem Namen stmts gesetzt. Für diese Sammlung habe ich eine Variation eines binären Suchbaums namens „Rot-Schwarz“ -Baum verwendet. (Weitere Informationen zu binären Suchbäumen finden Sie in meiner vorherigen Spalte zum Erstellen generischer Sammlungen.) Danach werden zwei zusätzliche Sammlungen erstellt – eine mit a Stack und eine mit a Vector . Der Stapel wird wie der Stapel in jedem Computer verwendet, aber der Vektor wird ausdrücklich für die Datenanweisungen im Basisprogramm verwendet. Die endgültige Sammlung ist ein weiterer rot-schwarzer Baum, der die Referenzen für die vom Basisprogramm definierten Variablen enthält. Dieser Baum ist die Symboltabelle, die vom Programm während der Ausführung verwendet wird.

Nach der Initialisierung werden die Eingabe- und Ausgabeströme eingerichtet, und wenn e nicht null ist, sammeln wir zunächst alle deklarierten Daten. Dies geschieht wie im folgenden Code gezeigt.

 /* First we load all of the data statements */ while (e.hasMoreElements()) { s = (Statement) e.nextElement(); if (s.keyword == Statement.DATA) { s.execute(this, in, pout); } }

Die obige Schleife betrachtet einfach alle Anweisungen, und alle gefundenen Datenanweisungen werden dann ausgeführt. Die Ausführung jeder Datenanweisung fügt die von dieser Anweisung deklarierten Werte in den Datenspeichervektor ein. Als nächstes führen wir das richtige Programm aus, das mit diesem nächsten Code ausgeführt wird:

 e = stmts.elements(); s = (Statement) e.nextElement(); do { int yyy; /* While running we skip Data statements. */ try { yyy = in.available(); } catch (IOException ez) { yyy = 0; } if (yyy != 0) { pout.println("Stopped at :"+s); push(s); break; } if (s.keyword != Statement.DATA) { if (traceState) { s.trace(this, (traceFile != null) ? traceFile : pout); } s = s.execute(this, in, pout); } else s = nextStatement(s); } while (s != null); }

Wie Sie im obigen Code sehen können, besteht der erste Schritt darin, e neu zu initialisieren. Es gibt Code, der nach ausstehenden Eingaben im Eingabestream sucht, damit der Fortschritt des Programms durch Eingabe des Programms unterbrochen werden kann, und dann prüft die Schleife, ob die auszuführende Anweisung eine Datenanweisung wäre. Wenn dies der Fall ist, überspringt die Schleife die Anweisung, da sie bereits ausgeführt wurde. Die ziemlich komplizierte Technik, zuerst alle Datenanweisungen auszuführen, ist erforderlich, da BASIC zulässt, dass die Datenanweisungen, die eine READ-Anweisung erfüllen, an einer beliebigen Stelle im Quellcode angezeigt werden. Wenn die Ablaufverfolgung aktiviert ist, wird schließlich ein Ablaufverfolgungsdatensatz gedruckt und die sehr wenig beeindruckende Anweisung s = s.execute(this, in, pout); aufgerufen. Das Schöne ist, dass der ganze Aufwand, die Basiskonzepte in leicht verständliche Klassen zu kapseln, den endgültigen Code trivial macht. Wenn es nicht trivial ist, haben Sie vielleicht eine Ahnung, dass es eine andere Möglichkeit gibt, Ihr Design zu teilen.

Einwickeln und weitere Gedanken

Der Interpreter wurde so konzipiert, dass er als Thread ausgeführt werden kann, sodass mehrere COCOA-Interpreter-Threads gleichzeitig in Ihrem Programmbereich ausgeführt werden können. Ferner können wir mit Hilfe der Funktionserweiterung ein Mittel bereitstellen, mit dem diese Threads miteinander interagieren können. Es gab ein Programm für den Apple II und später für den PC und Unix namens C-Robots, das ein System interagierender „Roboter“ -Entitäten war, die mit einer einfachen abgeleiteten Basissprache programmiert wurden. Das Spiel bot mir und anderen viele Stunden Unterhaltung, war aber auch eine hervorragende Möglichkeit, jüngeren Schülern (die fälschlicherweise glaubten, sie spielten nur und lernten nicht) die Grundprinzipien des Rechnens vorzustellen. Java-basierte Interpreter-Subsysteme sind viel leistungsfähiger als ihre Gegenstücke vor Java, da sie auf jeder Java-Plattform sofort verfügbar sind. COCOA lief auf Unix-Systemen und Macintoshes am selben Tag, an dem ich auf einem Windows 95-basierten PC arbeitete. Während Java durch Inkompatibilitäten in den Thread- oder Window Toolkit-Implementierungen verprügelt wird, wird oft Folgendes übersehen: Viel Code „funktioniert einfach.“

Chuck McManis ist derzeit Director ofsystem Software bei FreeGate Corp., einem venture-finanzierten Start-up, das Möglichkeiten auf dem Internetmarkt erforscht. Bevor er zu FreeGate kam, war Chuck Mitglied der Java-Gruppe. Er trat der Java-Gruppe kurz nach der Gründung von FirstPerson Inc. bei. und war Mitglied der Portable OS Group (der Gruppe, die für die OSportion von Java verantwortlich ist). Später, als FirstPerson aufgelöst wurde, blieb ermit der Gruppe durch die Entwicklung der Alpha- und Betaversionen der Java-Plattform. Er schuf die erste „all Java“ Homepage im Internet, als er im Mai 1995 die Javaversion der Sun Homepage programmierte. Er entwickelte auch eine kryptografische Bibliothek für Java und Versionen des Java Classloaders, die Klassen basierend auf digitalen Signaturen überprüfen konnten.Bevor er zu FirstPerson kam, arbeitete Chuck im Betriebssystembereich von SunSoft und entwickelte Netzwerkanwendungen, wo er das erste Design von NIS + machte. Schauen Sie sich auch seine Homepage an. :END_BIO

Weitere Informationen zu diesem Thema

  • Hier finden Sie Links zu den oben genannten Quelldateien:
    • Konstanter Ausdruck.java
    • Funktionsausdruck.java
    • Programm.java
    • Anweisung.java
    • StringExpression.java
    • Variable.java
    • VariableExpression.java
  • Und hier ist ein .ZIP-Datei der Quelldateien:
    indepth.zip
  • „Uncommon Lisp“ — ein in Java geschriebener Lisp-Interpreter
    http://user03.blue.aol.com/thingtone/workshop/lisp.htm
  • Mike Cowlishaws NetRexx Interpreter in Java geschrieben
    http://www2.hursley.ibm.com/netrexx/
  • Eine alte Kopie der USENET BASIC FAQ (es enthält immer noch nützliche Informationen.)
    http://whitworth.me.ic.ac.uk/people/students/djbur/qbasic.htm
  • COCOA, ein in Java geschriebener BASIC-Interpreter
    http://www.mcmanis.com/~cmcmanis/java/javaworld/examples/BASIC.html
  • Chucks Java-Ressourcen-Seite
    http://www.mcmanis.com/~cmcmanis/java/javaworld/
  • TCL-Interpreter in Java geschrieben
    http://www.cs.cornell.edu/home/ioi/Jacl/
  • :
  • “ So erstellen Sie einen Interpreter in Java, Teil 2Die Struktur“
    Der Trick zum Zusammenstellen der Foundation Classes für einen einfachen Interpreter.
  • „So erstellen Sie einen Interpreter in Java, Teil 1Die Grundlagen“
    Für komplexe Anwendungen, die eine Skriptsprache erfordern, kann Java verwendet werden, um den Interpreter zu implementieren und jeder Java-App Skriptfunktionen hinzuzufügen.
  • „Lexikalische Analyse, Teil 2anwendung erstellen“
    Verwendung des StreamTokenizer-Objekts zur Implementierung eines interaktiven Rechners.
  • “ Lexikalische Analyse und JavaPart 1″
    Erfahren Sie, wie Sie mit den Klassen StringTokenizer und StreamTokenizer lesbaren Text in maschinenlesbare Daten konvertieren.
  • „Wiederverwendung von Code und objektorientierte Systeme“
    Verwenden Sie eine Hilfsklasse, um dynamisches Verhalten zu erzwingen.
  • “ Container-Unterstützung für Objekte in Java 1.0.2″
    Das Organisieren von Objekten ist einfach, wenn Sie sie in Container legen. Dieser Artikel führt Sie durch das Design und die Implementierung eines Containers.
  • “ Die Grundlagen der Java-Klassenlader“
    Die Grundlagen dieser Schlüsselkomponente der Java-Architektur.
  • “ Garbage Collection wird nicht verwendet“
    Minimieren Sie das Heap-Thrashing in Ihren Java-Programmen.
  • „Threads und Applets und visuelle Steuerelemente“
    Dieser letzte Teil der Serie untersucht das Lesen mehrerer Datenkanäle.
  • „Verwenden von Kommunikationskanälen in Applets, Teil 3“
    Entwickeln Sie Techniken im Visual Basic-Stil für das Applet-Design – und konvertieren Sie sie dabei.
  • “ Synchronisieren von Threads in Java, Teil II“
    Erfahren Sie, wie Sie eine Datenkanalklasse schreiben und dann eine einfache Beispielanwendung erstellen, die eine reale Implementierung der Klasse veranschaulicht.
  • “ Synchronisieren von Threads in Java“
    Der ehemalige Java-Teamentwickler Chuck McManis führt Sie durch ein einfaches Beispiel, das veranschaulicht, wie Threads synchronisiert werden, um ein zuverlässiges und vorhersehbares Applet-Verhalten sicherzustellen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.