|
Implementation des BASIC-InterpretersDie Umsetzung des BASIC-Interpreters in JAVATM stellte für mich eine neue Herausforderung dar, zum einen war mir JAVATM neu und zum anderen war es schon immer mein Wunsch, etwas in Richtung Simulation älterer Softwaresysteme zu tun. Alle hier implementierten Funktionen wie REM, LET, Definition von Variablen, CLEAR, die Sprunganweisungen GOTO und GOSUB, die FOR - Schleife, die Anweisung IF, logische Verknüpfungen sowie INPUT und PRINT zur Datenein- und Datenausgabe sowie die mathematischen Funktionen Addition, Subtraktion, Division, Multiplikation, Quadratwurzel und Potenz stellen den Grundstock zur erfolgreichen Programmierung in BASIC dar. |
Grundkonzept des InterpretersAn dieser Stelle sollen einige Aspekte der Umsetzung des Interpreters erläutert werden, genaueren Einblick bieten direkt die Quelltexte und die darin enthaltenen Kommentare. Oberflächengestaltung und EreignisauswertungDas erste Problem war die Realisierung der Eingabe. Im Original-BASIC erfolgt die Eingabe des Programmtextes im Interpreter selbst (Kapitel 5), seine Abarbeitung wird durch den Befehl "RUN" gestartet. Ich habe mich dazu entschlossen ein separates Programmcode-Eingabefenster und ein Ausgabefenster zu nutzen. Das Eingabefenster ist mit einer Menüleiste und einer Button-Leiste versehen, die alle wichtigen Funktionen wie
Die Klasse "Emulator" stellt den Startpunkt des Interpreters dar. In ihr erfolgt der Aufbau des Anzeigefensters und des Eingabefensters und sie stellt die Routinen zur Reaktion auf die Menü- bzw. Button-Betätigung bereit:
dabei Nutzung eines "FileDialog"s und
Die Klasse "Ausfuehren" als zentrale SteuerschleifeDie erste Aufgabe der Klasse Ausfuehren besteht darin, den BASIC-Quelltext aus dem Eingabefenster zu holen. Dieser Quelltext wird in einem String erfaßt und seine Länge wird bestimmt.puffer = new String(GUI.eingabefeld.getText());Danach werden aus diesem Quelltext alle Zeilennummern extrahiert, dazu dient die Funktion "zeileneinlesen". Hier werden alle Zeilennummern und die Startposition des Quelltextes innerhalb einer Programmzeile ermittelt und in einer Hashtabelle abgespeichert, weiterhin werden alle Zeilennummern in einem Vektor vorgehalten - dieser wird später zum extrahieren der aktuellen Programmzeile benötigt. Damit wurde der "große" Quelltext in einzelne Programmzeilen zerlegt und alle Startpositionen des Programmtextes innerhalb einer Programmzeile sind bestimmt worden, damit muß bei der späteren Interpretation die Zeilennummer nicht mehr eingelesen und ausgewertet werden. Danach folgt die eigentliche Interpretation. Dazu werden der Reihe nach alle Programmzeilen abgearbeitet. Diese Abarbeitung wird durch zwei geschachtelte Schleifen realisiert. Die äußere Schleife ermittelt jeweils die aktuelle Zeilennummer und liest den zu dieser Zeilennummer gehörenden Programmtext in die Variable "teil_string" ein. z_zaehler=0;Die Variable "z_zaehler" wird vor Beginn der Schleife initialisiert, um eine spätere Manipulation, die einen Neustart der Schleife zur Folge hat, zu ermöglichen. Die zweite innere Schleife durchläuft die jeweils ausgewählte aktuelle Programmzeile. zaehler_teilstring=0+offset;Über "offset" kann eine gewünschte Position innerhalb der Programmzeile spezifiziert werden, dies wird erforderlich, um z.B. die Rückkehrposition nach der Bearbeitung eines "GOSUB" festzulegen und damit die weitere Abarbeitung der Programmzeile zu ermöglichen. Innerhalb dieser inneren Schleife werden mittels "switch" und verschiedenen IF-Abfragen Tests auf spezielle Sonderzeichen wie dem Doppelpunkt (als Befehlstrennsymbol), dem Ausrufezeichen (als Kennzeichnung von Kommentaren) und natürlich den verschiedenen BASIC-Befehlen durchgeführt und entsprechend verzweigt. Werden Befehle erkannt, so wird der Zeilenpositionszähler "zaehler_teilstring" um die Länge des Befehlswortes erhöht und der nachfolgende Text an die jeweiligen Befehlsroutinen übergeben. Die Befehlsroutinen passen ihrerseits die Schleifenvariablen an, damit nach der Rückkehr aus diesen Routinen die Interpretation an definierten Stellen des Quelltextes fortgesetzt werden kann. Ist eine Programmzeile abgearbeitet, wird durch die äußere Schleife die nächste Programmzeile eingelesen und ihre Interpretation durch die neu gestartete innere Schleife durchgeführt. Auf diese Weise wird das gesamte BASIC-Programm interpretiert, in der Klasse Ausfuehren wird jede Zeile bearbeitet, jeder Befehl wird erkannt und seine Bearbeitung ausgelöst - die Klasse Ausfuehren koordiniert die Befehlsabarbeitung. Die Klasse "Aus"Die in der Klasse Aus enthaltene Routine "Aus" wird von verschiedenen Programmpunkten aus angesprungen. Sie setzt die Schleifenzähler zaehler_teilstring und z_zaehler der Klasse Ausfuehren auf ihre jeweiligen Endwerte teil_string.length() und z_size. Dies bewirkt die sofortige Beendigung der Programmabarbeitung.VariablenVariablen werden anders als im Original-Interpreter innerhalb einer Hashtabelle abgelegt, einerseits ist die Handhabung einer solchen Tabelle einfacher und andererseits sind Zugriffe auf Variablen damit schneller. Ebenfalls unterschiedlich im Vergleich zum Original ist die Länge der Variablennamen, hier werden alle Zeichen des Namens beachtet und verarbeitet.Wie im Originalinterpreter erfolgt die Behandlung der Groß- und Kleinschreibung - Groß- und Kleinbuchstaben werden immer als ein und dasselbe Zeichen behandelt. Ebenso erfolgt die Überprüfung der Variablennamen auf reservierte Wörter, zu diesen zählen: AT, FN, IF, LN, ON, OR, PI und TO. Zu Programmbeginn wird die Konstante Pi mit dem Wert des Originalinterpreters initialisiert, Pi = 3,14159. Die Definition und Ablage der Variablen erfolgt innerhalb der Klasse
Ausfuehren. Nachdem die aktuelle Stelle im Programmtext auf alle möglichen
Befehle und Sonderzeichen hin untersucht wurde und aufgrund des Auftretens
von Buchstaben nur noch die Möglichkeit einer Variablendefinition
gegeben ist, wird in die Funktion "variable()" verzweigt. In dieser Funktion
wird zuerst der Variablenname eingelesen.
while ( teil_string.charAt(zaehler_teilstring)!=' ' && teil_string.charAt(zaehler_teilstring)!='=' ){Danach erfolgt in gleicher Weise das Einlesen des Variablenwertes, nachdem auf ein notwendiges Gleichheitszeichen getestet wurde. Anschließend wird auf numerische oder String-Variablen getestet und die Variablenwerte entsprechend berechnet. Abschließend wird die Variable mit ihrem Wert in der Variablentabelle vermerkt.var = var + teil_string.charAt(zaehler_teilstring);} vartab.put(var,varwert); |
Implementierte BefehleENDZur Beendigung eines BASIC-Programms dient die Anweisung "END". Beim Auftreten dieser Anweisung wir aus der Klasse Aus die Routine "Aus" aufgerufen. Dabei werden die Schleifenzähler zaehler_teilstring und z_zaehler auf ihre Endwerte gesetzt. Dies bewirkt die sofortige Beendigung der Programmabarbeitung.CLSDie Funktion "Cls" wird über die Klasse Cls realisiert. Hier werden alle Elemente die im Darstellungs-Vektor z enthalten sind gelöscht und die Zeilenposition, die durch die Variable y in der Klasse Anzeige repräsentiert wird, auf ihren Startwert gesetzt.REMDie Anweisung REM steht für eine Kommentarzeile. Ihre Implementation ist wahrscheinlich die einfachste ihrer Art. Trifft der Interpreter auf eine solche REM-Zeile, setzt er den Zähler des aktuellen Teil-Strings, der die aktuelle Programmzeile repräsentiert, auf seinen Endwert.zaehler_teilstring=teil_string.length();Dadurch wird ein Abbruch der Abarbeitung der aktuellen Programmzeile erwirkt und der Programmfluß geht in der folgenden Zeile weiter. LET"LET" ist die Alternative Möglichkeit zur Definition von Variablen. Das Wort "LET" und nachfolgende Leerzeichen werden im Programmtext einfach überlesen und danach wird in die Routine zur Variablendefinition verzweigt.zaehler_teilstring += 3; // LET ueberlesen CLEARMit Hilfe von CLEAR wird der gesamte Variablenspeicher gelöscht. Hier wird die Hashtabelle "vartab", die alle zuvor definierten Variablen enthält gelöscht.vartab.clear(); Sprunganweisungen GOTO und GOSUBBei einem GOTO wird zunächst die Zeilennummer eingelesen, zu der gesprungen werden soll, dies geschieht mit Hilfe einer while-Schleife in Verbindung mit einer Switch-Anweisung.while ( z < teil.length() && abbruch == false) {Über die Funktion "zeigersichern" wird getestet, ob die angeforderte Zeilennummer existiert und gegebenenfalls eine Fehlermeldung und der Programmabbruch generiert. Ist die Zeilennummer vorhanden, wird die Variable "z_zaehler" aus der Klasse Ausfuehren entsprechend gesetzt. Zurückgekehrt aus der Funktion "zeigersichern" wird der Zeiger "zaehler_teilstring" in der Klasse Ausfuehren auf seinen Endwert gesetzt, um so den Abbruch der Bearbeitung der aktuellen Programmzeile zu erreichen.switch (teil.charAt(z)){} // ende whilecase '0': case '1': case '2': case '3': case '4':} // ende switch Die Klasse GOSUB, als Funktion für einen Unterprogrammaufruf, erbt die Funktionalität der Klasse GOTO. In der Klasse GOSUB wird nur die Funktion "zeigersichern" überschrieben. Hier wird wie vorher der Zeilennummern-Test durchgeführt und zusätzlich werden die Rückkehrparameter, welche nach einem RETURN die Abarbeitung hinter dem GOSUB garantieren, in einem Stack gesichert. Ausfuehren.gosub_stack.push(String.valueOf(Ausfuehren.zaehler_teilstring)); RETURNRETURN ist der Abschluß eines Unterprogramms. Hier werden die Zeilenparameter vom Stack geholt und den Variablen "z_zaehler" und "offset" zugewiesen. Damit wird die entsprechende Programmzeile angesprochen und durch "offset" die Position innerhalb der Zeile festgelegt. Tritt beim Lesen vom Stack ein Fehler auf, so waren keine Parameter auf ihm vorhanden, dies ist die Folge eines RETURNs ohne vorangehendes GOSUB. In diesem Fall wird eine Fehlermeldung generiert und der Programmablauf gestoppt. Zum Abschluß wird noch der momentan gültige Zähler "zaehler_teilstring" auf seinen Endwert gesetzt und damit die Abarbeitung mit den neuen Parametern gestartet.try {z_zaehler = Integer.parseInt(String.valueOf(gosub_stack.pop()),10)-1;} catch (Exception e){ FOR - SchleifeDie FOR - Schleife wird durch die zwei Klassen For und Next realisiert. In der Klasse For wird zunächst der Name und der Startwert der Laufvariablen eingelesen und entsprechend in den Variablen "varname" und "varist" eingelesen. Da der Startwert auch aus einer mathematischen Formel bestehen kann, z.B. i+3, wird der eingelesene varist-String an die Funktion "Berechne_zahl" aus der Klasse Berechne_zahl übergeben, Rückgabewert ist unser endgültiger Startwert.Bei diesem Schritt wird gleichzeitig auf eine mögliche Angabe einer Schrittweite getestet. z = teil.indexOf("step");Ist die Anweisung "STEP" vorhanden, so wird zusätzlich zum Startwert der Schrittweitenparameter eingelesen, wie schon bei dem Startwert-Parameter berechnet und nachfolgend in der Variablen "varstep" abgelegt. Ist kein "STEP" vorhanden erfolgt nur das Einlesen des Startwertes und die Schrittweite wird auf den Default-Wert "1" gesetzt. Abschließend wird die Laufvariable mit ihrem aktuellen Wert in der Variablentabelle gespeichert, die Positionszeiger für die aktuelle Zeilennummer und die Position innerhalb dieser Zeile werden bestimmt und die schleifenrelevanten Parameter werden auf den Stack gelegt. Zu den schleifenrelevanten Parametern gehören:
Der nächste Schritt ist das Erreichen eines "NEXT" im Programmtext. Die Klasse Next stellt den Befehl "NEXT" bereit. Hier werden zu Beginn alle Parameter betreffend der Laufvariablen vom Stack bzw. aus der Variablentabelle geholt. varstep = Double.valueOf((String)For.for_stack.pop()).doubleValue();Es folgt im Anschluß die Neuberechnung des Laufparamters, das Abspeichern des neuen Variablenwertes in die Variablentabelle und der Test, ob der neue Wert den Soll-Wert überschritten hat. Ist dies nicht der Fall, werden die Schleifenparameter "z_zaehler" und "offset" der Klasse Ausfuehren so gesetzt, daß eine Abarbeitung hinter dem FOR ... TO ... STEP möglich wird und die Laufvariablenparameter werden auf dem Stack vermerkt, anderenfalls wird der Stack bereinigt und die Programmabarbeitung wird hinter dem "NEXT" fortgesetzt. Die Anweisung IF und logische VerknüpfungenDie {\em Klassen If und Vergleich} stellen die Funktionalität der IF-Anweisungund der damit verbundenen logischen Verknüpfungen zur Verfügung. Bei einem "`IF"' im Programm wird die Funktion "`If"' der Klasse If aufgerufen. Hier wird als Erstes getestet, ob ein "`THEN"' vorhanden ist, wenn nicht wird ein Fehler ausgegeben. Danach wird die zu vergleichende Zeichenkette eingelesen und an die Klasse Vergleich übergeben, ihr Rückgabewert wird der Variablen "`wahrheitswert"' zugeordnet. Je nachdem, ob dieser "`true"' oder "`false"' ist, wird entweder hinter "`THEN"' oder hinter einem optionalem "`ELSE"' fortgefahren. Ist kein "`ELSE"' vorhanden und ist der Wahrheitswert "`false"', wird mit der nächsten Programmzeile fortgefahren. Tritt im späteren Programmtext ein "ELSE" auf, wird die Funktion "Elseanweisung" in der Klasse Ifanweisung aufgerufen. Dort werden der Zähler für die Position in der aktuellen Programmzeile "`zaehler_teilstring"' auf seinen Endwert gesetzt, eine Abarbeitung des Programms hinter dem ELSE-Zweig wird damit möglich. Der eigentliche Vergleich findet in der Klasse Vergleich statt. Hier werden Stringvariablen und Zahlen eingelesen und miteinander verglichen. Für jeden auftretenden Vergleichsoperator <, >, >=, >= und <> werden entsprechende Flags gesetzt. Ein Beispiel soll die Funktionsweise erläutern: "Hallo" < "Auto" Bei diesem Beispiel wird die Zeichenkette "`Hallo"' eingelesen, es wird festgestellt, daß es sich um einen String handelt und anschließend die Routine ausrechnen_string() aufgerufen. In dieser Routine wird auf ein möglicherweise gesetztes Operator-Flag getestet. Ist keines gesetzt, wie in diesem Fall, wird die aktuelle eingelesen Zeichenkette auf einen Stack gelegt. Als nächstes wird festgestellt, daß es sich um ein "kleiner als" handelt, dementsprechend wird das Kleiner-Flag gesetzt. Nun wird die zweite Zeichenkette eingelesen und dann in die Routine ausrechnen_string() verzweigt. Hier wird festgestellt, daß das Kleiner-Flag gesetzt ist. Damit wird die vorhergehende Zeichenkette vom Stack geholt und mit der aktuellen Zeichenkette verglichen, der dabei entstehende Wahrheitswert wird auf den Stack gelegt. Die weiteren Verknüpfungen verlaufen analog. Ist kein weiterer Vergleich vorgesehen, wird die Routine ausgabe() ausgeführt. Sie holt das Ergebnis vom Stack und gibt einen entsprechenden String an die aufrufende Routine zurück. INPUT zur DateneingabeDie Dateneingabe wird in BASIC über die Anweisung "INPUT" ermöglicht, in der JAVATM - Umsetzung durch die Klassen InputFenster und Input. Bei Erreichen des Befehls "INPUT" im Programmtext wird zunächst die aktuelle Zeilenposition gespeichert, um an dieser Stelle nach der Eingabe weiterarbeiten zu können, dabei erfolgt eine Überprüfung, die feststellt, ob der Befehl "INPUT" alleinstehend ist, wenn ja liegt ein Fehler vor und eine entsprechende Meldung wird generiert.Danach wird die Funktion InputFenster der Klasse InputFenster aufgerufen und die Abarbeitung des Quelltextes unterbunden, indem die Schleifenvariablen in der Klasse Ausfuehren auf ihre Endwerte gesetzt werden. abbruch=true;Der INPUT-Befehl erlaubt optional die Angabe eines Eingabetextes, Beispiel: INPUT "'Ihre Eingabe"'; a$Dieser wird, sofern er vorhanden ist, in der Funktion InputFenster eingelesen und als Eingabetext vorgemerkt. Danach wird ein Eingabefenster geöffnet, das als Fenstertitel den Text "Input" trägt. Der Eingabetext wird vor dem im Fenster enthaltenen Eingabefeld positioniert. Wurde kein optionale Eingabetext angegeben, wird dieser mit dem String "?" versehen. Ebenfalls wird der Name der Eingabevariablen eingelesen. Ist kein Variablenname vorhanden, wird ein Fehler angezeigt. Vor dem Aufbau des Eingabefensters wird ein ActionListener erzeugt. eingabe.addActionListener(new Input(this, anzeige));Im Falle eines "`Enter"' im Eingabefeld wird die Steuerung an die Klasse Input übergeben. In der Klasse Input wird die Eingabe auf ihre Korrektheit überprüft. Hier wird auf Stringvariablen und numerische Variablen getestet. War z.B. die Eingabevariable eine numerische, so dürfen in der Eingabe keine Buchstaben oder Sonderzeichen vorhanden sein, einzige Ausnahme ist ein "E" bzw. "e" als Kennzeichnung des Exponenten. String darf hingegen alles zugewiesen werden. Im Fehlerfall wird eine Fehlermeldung "`REDO FROM START"' generiert und eine erneute Eingabe erzwungen. Bei erfolgreicher und korrekter Eingabe werden die Schleifenparameter in der Klasse Ausfuehren neu gesetzt, um die weitere Abarbeitung des BASIC-Programms zu ermöglichen. Ausfuehren.offset = string_laenge;Danach wird das Eingabefenster geschlossen, die Eingabevariable mit ihrem Wert in die Variablentabelle eingetragen, die Ausgabe des Eingabetextes in das Programmfenster realisiert und die Programminterpretation neu gestartet. PRINT zur DatenausgabeDie Funktion "PRINT" stellt eine elementare Anweisung des BASIC-Befehlssatzes dar, die Klasse Print realisiert diese Funktionalität. Erfaßt werden Stringverknüpfungen mittels "+", Variablenausgabe, Berechnungen und Ausgabeverknüpfungen unter Zuhilfenahme von " ; " und " , ". Durch das "+" werden Strings und Stringvariablen aneinandergereiht, " ; " bewirkt einen unmittelbaren Anschluß der nach dem " ; " folgenden Ausgabe. Der Parameter " , " hat eine analoge Funktion zu " ; " nur mit dem Unterschied, das einige Leerzeichen zwischen die Ausgaben gesetzt werden.Einige Beispiele:
if (s.length()==0) {Ist der die Länge des an die Funktion Print übergebenen String "s" gleich 0, so war es ein alleinstehendes "PRINT", der String "s" repräsentiert alles nach dem Befehl "PRINT" stehende. Ist die Länge des Strings "s" ungleich 0, so folgt zumindest noch ein Leerzeichen.hilf="'"';} // ende if Anschließend erfolgt die Bearbeitung des Strings s innerhalb einer Schleife, die den String durchläuft, und einer darin eingeschlossenen switch-Anweisung. Trifft man auf einen Doppelpunkt (Doppelpunkt = Befehlstrennzeichen), so ist der Print-Befehl beendet und die Abarbeitung kann hinter dem Doppelpunkt fortgeführt werden. Bei einem "+" oder einem " ; " wird die aktuelle Ausgabe, enthalten in der Variablen "hilf", mit einer vorhergehenden verknüpft und die Flags "plus" und "erweiterung" werden entsprechend gesetzt. case '+': plus=true;Bei einem Komma werden zusätzlich Leerzeichen eingebaut.endausgabe += hilf;case ';': erweiterung = true; case ',': erweiterung = true;Die genannten Flags werden später zur Identifizierung eines Type Mismatchs verwendet um Typkonflikte zwischen Strings und Zahlen zu erkennen.endausgabe = endausgabe + hilf + " "; if (plus==true && erweiterung == false) { Im weiteren Verlauf wird auf Variablen getestet. Diese werden mitsamt möglicher Operationszeichen eingelesen und auf ihre Typzugehörigkeit geprüft, ihre Werte werden berechnet und der Variablen "hilf" zugeordnet. Ebenso werden Zahlen mit Operationszeichen eingelesen und berechnet. Die Berechnung erfolgt unter Zuhilfenahme der Klassen Berechne_string und Berechne_zahl.Aus.Aus(); Der Abschluß eines Print bildet das Anpassen des Zählers "zaehler_teilstring" und der Rückgabe des auszugebenden Strings. Ausfuehren.zaehler_teilstring += i-1;Das Auslösen der Ausgabe erfolgt dann wieder in der Klasse Ausfuehren durch Übergabe des Rückgabestrings "ausgabe" an die Klasse Anzeige. ausgabe = print.Print(anzeige,teil\_string.substring(zaehler\_teilstring)); Funktionen zur Berechnung von Zahlen und StringsDie Stringberechnung erfolgt mit Hilfe der Klasse Berechne_string. Hier werden Zeichenketten und Stringvariablen eingelesen. Trifft das Programm auf ein "+" heißt dies, daß zwei Strings miteinander verknüpft werden sollen, dazu wird ein Flag "plus" auf true gesetzt. Bei jedem Einlesen eines Strings wird das Plus-Flag getestet, ist es gesetzt, wird der aktuelle String auf den Stack gelegt, ist das Flag nicht gesetzt, wird der Stack geleert und nur das eine Element auf den Stack gelegt. Zum Abschluß wird die Routine ausgabe() aufgerufen. Sie liest alle auf dem Stack befindlichen Zeichenketten aus und fügt diese aneinander, danach wird dieser String an die rufende Funktion zurückgegeben.Zur Zahlenberechnung wird die Klasse Berechne_zahl bemüht. Hier werden zuerst Klammerausdrücke beseitigt, indem die entsprechenden Klammerausdrücke wieder an die Berechnungsroutine übergeben werden - die Klammerausdrücke werden rekursiv eliminiert. Als Ergebnis erhalten wir am Ende einen String, der keine Klammerausdrücke enthält. Der nächste Schritt ist die Beseitigung von Wurzelausdrücken, im Anschluß daran wird der entstandene String, der keine Klammern und Wurzeln enthält, berechnet. Die Berechnung ist ähnlich der Stringberechnung, nur wurden hier die Operationen Multiplikation, Division, Subtraktion und Potenz hinzugefügt. |
AusblickZur weiteren Verfeinerung des Programms bieten sich weitere mathematische oder String-Funktionen, Arrays, DATA - Zeilen, die logischen Verknüpfungen "und"/"oder" und Grafikbefehle an. Weiterhin ist die Erweiterung des "NEXT" möglich - ein NEXT Y wäre dann zum Beispiel möglich und negative Schrittweiten könnten ermöglicht werden. Auf jeden Fall bietet das Programm noch Spielraum für Erweiterungen und den damit verbundenen Lerneffekt bezüglich JAVATM.
|
|