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

Zurück 1 2 3 Page 2 Weiter Seite 2 von 3

Andere Aspekte: Strings und Arrays

Zwei weitere Teile der BASIC-Sprache 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 ExpressionKlasse so geändert, dass sie den Begriff "Zeichenfolgen" -Ausdrücke enthält. Diese Änderung erfolgte in Form von zwei Ergänzungen: isStringund stringValue. Die Quelle für diese beiden neuen Methoden ist unten angegeben.

String stringValue (Program pgm) löst BASICRuntimeError aus {throw new BASICRuntimeError ("Keine String-Darstellung dafür."); } boolean isString () {return false; }}

Für ein BASIC-Programm ist es natürlich 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 zu Expressioneiner Unterklasse von gehörten und Expressionstattdessen zu dieser gehörten. Durch Einfügen dieser beiden Methoden in die Basisklasse Expressionkönnen jedoch alle Objekte getestet werden, um festzustellen, ob es sich tatsächlich um Zeichenfolgen handelt.

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

String stringValue (Programm pgm) löst BASICRuntimeError aus {StringBuffer sb = new StringBuffer (); sb.append (this.value (pgm)); return sb.toString (); }}

Und wenn der obige Code verwendet wird, können Sie die Verwendung von eliminieren, isStringda jeder Ausdruck einen Zeichenfolgenwert zurückgeben kann. Außerdem können Sie die valueMethode ändern , um zu versuchen, eine Zahl zurückzugeben, wenn der Ausdruck eine Zeichenfolge ergibt, indem Sie ihn durch die valueOfMethode von ausführen java.lang.Double. 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 Dolmetschers treffen. In BASIC muss der Interpreter einen Fehler zurückgeben, wenn einer numerischen Variablen eine Zeichenfolge zugewiesen wird. Daher habe ich den ersten Ansatz gewählt (Rückgabe eines Fehlers).

Für Arrays gibt es verschiedene Möglichkeiten, wie Sie Ihre Sprache so gestalten können, dass sie interpretiert werden. C verwendet die eckigen Klammern um Array-Elemente, um die Indexreferenzen des Arrays von Funktionsreferenzen zu unterscheiden, deren Argumente in Klammern stehen. Die Sprachdesigner von BASIC haben sich jedoch dafür entschieden, Klammern sowohl für Funktionen als auch für Arrays zu verwenden. Wenn 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 zunächst annimmt, dass es sich um Funktionen handelt, und dies testet. Anschließend 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" definiert. Jede Variable, deren Name mit einem Funktionsnamen übereinstimmt, wird stattdessen vom lexikalischen Analysator 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, entfernen wir die Zeichenfolge " MYARRAY ( 2 )'nicht als gültiges Array interpretiert werden (beachten Sie den Abstand zwischen dem Variablennamen und der offenen Klammer).

Der letzte Trick zum Implementieren von Arrays liegt in der VariableKlasse. Diese Klasse wird für eine Instanz einer Variablen verwendet, und wie ich in der Spalte des letzten Monats erläutert habe, handelt es sich um eine Unterklasse von Token. Es gibt jedoch auch einige Maschinen zur Unterstützung von Arrays, und das werde ich unten zeigen:

Klasse Variable erweitert Token {// Zulässige Variablentypen 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; / * * Wenn sich die Variable in der Symboltabelle befindet, werden * diese Werte initialisiert. * / int ndx []; // Array-Indizes. int mult []; // Array-Multiplikatoren double nArrayValues ​​[]; String sArrayValues ​​[];

Der obige Code zeigt die einer Variablen zugeordneten Instanzvariablen wie in der ConstantExpressionKlasse. 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 VariableKlasse zu erstellen , die nur skalare Variablen enthält, und dann eine ArrayVariableUnterklasse hinzuzufügen , um die Feinheiten von Arrays zu behandeln. Ich entschied mich, sie zu kombinieren und skalare Variablen im Wesentlichen in Arrays der Länge 1 umzuwandeln.

Wenn Sie den obigen Code lesen, sehen Sie Array-Indizes und Multiplikatoren. Dies liegt daran, dass mehrdimensionale Arrays in BASIC mithilfe eines einzelnen linearen Java-Arrays implementiert werden. Der lineare Index in das Java-Array wird manuell unter Verwendung der Elemente des Multiplikator-Arrays berechnet. Die im BASIC-Programm verwendeten Indizes werden auf ihre Gültigkeit überprüft, indem sie mit dem maximalen zulässigen Index im ndx- Array der Indizes verglichen werden .

In einem BASIC-Array mit drei Dimensionen von 10, 10 und 8 würden 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 BASIC-Programm verwendete Nummer mit der maximalen zulässigen Nummer 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 zum Abbilden von einer mehrdimensionalen Array-Indexspezifikation in eine lineare Array-Indexspezifikation verwendet werden. 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 VariableKlasse ist unten dargestellt.

 Ausdruck expns []; 

Das expns- Array wird verwendet, um Arrays zu behandeln, die als " A(10*B, i)." Geschrieben sind . In diesem Fall handelt es sich bei den Indizes eher um Ausdrücke als um 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 []) löst BASICRuntimeError aus {int offset = 0; if ((ndx == null) || (ii.length! = ndx.length)) wirft einen neuen BASICRuntimeError ("Falsche Anzahl von Indizes."); for (int i = 0; i <ndx.length; i ++) {if ((ii [i] ndx [i])) wirft einen neuen BASICRuntimeError ("Index außerhalb des Bereichs"); Offset = Offset + (ii [i] -1) * mult [i]; } return offset; }}

Wenn Sie sich den obigen Code ansehen, werden Sie feststellen, dass der Code zuerst überprüft, ob die richtige Anzahl von Indizes verwendet wurde, wenn auf das Array verwiesen wurde, und dass dann 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 numValueund stringValuegeben einen Wert aus der Variablen als Zahl bzw. Zeichenfolge zurück. Diese beiden Methoden werden unten gezeigt.

double numValue (int ii []) löst BASICRuntimeError aus {return nArrayValues ​​[computeIndex (ii)]; } String stringValue (int ii []) löst BASICRuntimeError aus {if (subType == NUMBER_ARRAY) return "" + nArrayValues ​​[computeIndex (ii)]; return sArrayValues ​​[computeIndex (ii)]; }}

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

Der Java-Code ist recht unkompliziert, da er einen Großteil der Komplexität der Implementierung jedes einzelnen Teils verbirgt, wenn es endlich Zeit ist, das BASIC-Programm auszuführen.

Code ausführen

Der Code zum Interpretieren und Ausführen der BASIC-Anweisungen ist in der enthalten

run

Methode der

Program

Klasse. Der Code für diese Methode wird unten gezeigt, und ich werde ihn durchgehen, um auf die interessanten Teile hinzuweisen.

1 öffentlicher Leerlauf (InputStream in, OutputStream out) löst BASICRuntimeError aus {2 PrintStream pout; 3 Aufzählung e = stmts.elements (); 4 stmtStack = neuer Stack (); // nehme keine gestapelten Anweisungen an ... 5 dataStore = new Vector (); // ... und keine zu lesenden Daten. 6 dataPtr = 0; 7 Aussage s; 8 9 vars = new RedBlackTree (); 10 11 // wenn das Programm noch nicht gültig ist. 12 if (! E.hasMoreElements ()) 13 return; 14 15 if (out Instanz von PrintStream) {16 pout = (PrintStream) out; 17} else {18 pout = neuer PrintStream (out); 19}

Der obige Code zeigt, dass die runMethode ein InputStreamund ein OutputStreamzur Verwendung als "Konsole" für das ausführende Programm verwendet. In Zeile 3 wird das Aufzählungsobjekt 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 verwendet, der als "rot-schwarzer" Baum bezeichnet wird. (Weitere Informationen zu binären Suchbäumen finden Sie in meiner vorherigen Spalte zum Erstellen generischer Sammlungen.) Anschließend werden zwei zusätzliche Sammlungen erstellt - eine mit a Stackund eine mit aVector. Der Stapel wird wie der Stapel in jedem Computer verwendet, aber der Vektor wird ausdrücklich für die DATA-Anweisungen im BASIC-Programm verwendet. Die letzte Sammlung ist ein weiterer rot-schwarzer Baum, der die Referenzen für die vom BASIC-Programm 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 Ausgabestreams eingerichtet. Wenn e nicht null ist, erfassen wir zunächst alle deklarierten Daten. Dies geschieht wie im folgenden Code gezeigt.

/ * Zuerst laden wir alle Datenanweisungen * / while (e.hasMoreElements ()) {s = (Anweisung) e.nextElement (); if (s.keyword == Statement.DATA) {s.execute (this, in, pout); }}

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

e = stmts.elements (); s = (Anweisung) e.nextElement (); do {int yyy; / * Während der Ausführung überspringen wir Datenanweisungen. * / try {yyy = in.available (); } catch (IOException ez) {yyy = 0; } if (yyy! = 0) {pout.println ("Angehalten bei:" + s); Push (s); Unterbrechung; } if (s.keyword! = Statement.DATA) {if (traceState) {s.trace (this, (traceFile! = null)? traceFile: pout); } s = s.execute (dies, in, Schmollmund); } else s = nextStatement (s); } while (s! = null); }}

Wie Sie im obigen Code sehen können, besteht der erste Schritt darin, e neu zu initialisieren . Der nächste Schritt besteht darin, die erste Anweisung in die Variable s zu holen und dann in die Ausführungsschleife einzutreten. Es gibt einen Code, der auf ausstehende Eingaben im Eingabestream überprüft werden muss, damit der Programmfortschritt durch Eingabe des Programms unterbrochen werden kann. Anschließend prüft die Schleife, ob die auszuführende Anweisung eine DATA-Anweisung ist. Wenn dies der Fall ist, überspringt die Schleife die Anweisung, da sie bereits ausgeführt wurde. Die ziemlich komplizierte Technik, alle Datenanweisungen zuerst auszuführen, ist erforderlich, da BASIC zulässt, dass die DATA-Anweisungen, die eine READ-Anweisung erfüllen, an einer beliebigen Stelle im Quellcode angezeigt werden. Wenn schließlich die Ablaufverfolgung aktiviert ist, wird ein Ablaufverfolgungsdatensatz gedruckt und die sehr unscheinbare Anweisungs = s.execute(this, in, pout);wird aufgerufen. Das Schöne ist, dass der 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 einen anderen Weg gibt, Ihr Design zu teilen.

Zusammenfassung und weitere Gedanken

Der Interpreter wurde so konzipiert, dass er als Thread ausgeführt werden kann. Daher können mehrere COCOA-Interpreter-Threads gleichzeitig in Ihrem Programmbereich ausgeführt werden. Darüber hinaus können wir durch die Verwendung der Funktionserweiterung ein Mittel bereitstellen, mit dem diese Threads miteinander interagieren können. Es gab ein Programm für Apple II und später für PC und Unix namens C-Robots, bei dem es sich um ein System interagierender "Roboter" -Entitäten handelte, die mit einer einfachen BASIC-Derivatsprache 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 Macintosh-Computern am selben Tag, an dem ich an einem Windows 95-basierten PC arbeitete. Während Java durch Inkompatibilitäten in den Thread- oder Windows-Toolkit-Implementierungen verprügelt wird, wird häufig Folgendes übersehen: Viel Code "funktioniert einfach".