So erstellen Sie einen Interpreter in Java, Teil 1: Die BASICs

Als ich einem Freund erzählte, dass ich einen BASIC-Dolmetscher in Java geschrieben hatte, lachte er so heftig, dass er fast das Soda verschüttete, das er über seine Kleidung hielt. "Warum in aller Welt würden Sie einen BASIC-Interpreter in Java erstellen?" war die vorhersehbare erste Frage aus seinem Mund. Die Antwort ist sowohl einfach als auch komplex. Die einfache Antwort ist, dass es Spaß gemacht hat, einen Dolmetscher in Java zu schreiben, und wenn ich einen Dolmetscher schreiben würde, könnte ich genauso gut einen schreiben, an den ich mich aus den Anfängen des Personal Computing gut erinnere. Auf der komplexen Seite habe ich festgestellt, dass viele Leute, die heute Java verwenden, den Punkt überschritten haben, um taumelnde Duke-Applets zu erstellen, und sich ernsthaften Anwendungen zuwenden. Beim Erstellen einer Anwendung möchten Sie häufig, dass diese konfigurierbar ist.Der Mechanismus der Wahl für die Neukonfiguration ist eine Art dynamische Ausführungs-Engine.

Die dynamische Ausführung, die als Makrosprachen oder Konfigurationssprachen bezeichnet wird, ist die Funktion, mit der eine Anwendung vom Benutzer "programmiert" werden kann. Der Vorteil einer dynamischen Ausführungs-Engine besteht darin, dass Tools und Anwendungen angepasst werden können, um komplexe Aufgaben auszuführen, ohne das Tool zu ersetzen. Die Java-Plattform bietet eine Vielzahl von Optionen für dynamische Ausführungsmodule.

HotJava und andere heiße Optionen

Lassen Sie uns kurz einige der verfügbaren Optionen für die dynamische Ausführungs-Engine untersuchen und uns dann die Implementierung meines Interpreters genauer ansehen. Eine dynamische Ausführungs-Engine ist ein eingebetteter Interpreter. Ein Dolmetscher benötigt drei Einrichtungen, um arbeiten zu können:

  1. Ein Mittel, um mit Anweisungen geladen zu werden
  2. Ein Modulformat zum Speichern der auszuführenden Anweisungen
  3. Ein Modell oder eine Umgebung für die Interaktion mit dem Hostprogramm

HotJava

Der bekannteste eingebettete Interpreter muss die HotJava-Applet-Umgebung sein, die die Art und Weise, wie Menschen Webbrowser betrachten, grundlegend verändert hat.

Das HotJava-Applet-Modell basierte auf der Vorstellung, dass eine Java-Anwendung eine generische Basisklasse mit einer bekannten Schnittstelle erstellen und dann Unterklassen dieser Klasse dynamisch laden und zur Laufzeit ausführen könnte. Diese Applets stellten neue Funktionen zur Verfügung und stellten innerhalb der Grenzen der Basisklasse eine dynamische Ausführung bereit. Diese dynamische Ausführungsfähigkeit ist ein wesentlicher Bestandteil der Java-Umgebung und eines der Dinge, die sie so besonders machen. Wir werden uns diese spezielle Umgebung in einer späteren Spalte genauer ansehen.

GNU EMACS

Bevor HotJava eintraf, war GNU EMACS die vielleicht erfolgreichste Anwendung mit dynamischer Ausführung. Die LISP-ähnliche Makrosprache dieses Editors ist für viele Programmierer zu einem festen Bestandteil geworden. Kurz gesagt besteht die EMACS LISP-Umgebung aus einem LISP-Interpreter und vielen Funktionen vom Typ "Bearbeiten", mit denen die komplexesten Makros erstellt werden können. Es sollte nicht als überraschend angesehen werden, dass der EMACS-Editor ursprünglich in Makros geschrieben wurde, die für einen Editor namens TECO entwickelt wurden. Die Verfügbarkeit einer umfangreichen (wenn auch nicht lesbaren) Makrosprache in TECO ermöglichte es daher, einen völlig neuen Editor zu erstellen. Heute ist GNU EMACS der Basiseditor, und ganze Spiele wurden nur in dem als el-Code bekannten EMACS LISP-Code geschrieben. Diese Konfigurationsfähigkeit hat GNU EMACS zu einem Haupteditor gemacht.Während die VT-100-Terminals, auf denen sie ausgeführt werden sollen, zu bloßen Fußnoten in der Spalte eines Schriftstellers geworden sind.

REXX

Eine meiner Lieblingssprachen, die nie ganz für Furore gesorgt hat, war REXX, entworfen von Mike Cowlishaw von IBM. Das Unternehmen benötigte eine Sprache zur Steuerung von Anwendungen auf großen Mainframes, auf denen das VM-Betriebssystem ausgeführt wird. Ich habe REXX auf dem Amiga entdeckt, wo es über "REXX-Ports" eng mit einer Vielzahl von Anwendungen verbunden war. Über diese Ports konnten Anwendungen über den REXX-Interpreter remote gesteuert werden. Diese Kopplung von Dolmetscher und Anwendung führte zu einem viel leistungsfähigeren System, als es mit seinen Bestandteilen möglich war. Glücklicherweise lebt die Sprache in NETREXX weiter, einer Version, die Mike geschrieben hat und die in Java-Code kompiliert wurde.

Als ich mir NETREXX und eine viel frühere Sprache (LISP in Java) ansah, fiel mir auf, dass diese Sprachen wichtige Teile der Java-Anwendungsgeschichte bildeten. Gibt es eine bessere Möglichkeit, diesen Teil der Geschichte zu erzählen, als hier etwas zu tun, das Spaß macht - wie BASIC-80 wiederzubeleben? Noch wichtiger ist es, eine Möglichkeit aufzuzeigen, wie Skriptsprachen in Java geschrieben werden können, und durch ihre Integration in Java zu zeigen, wie sie die Funktionen Ihrer Java-Anwendungen verbessern können.

GRUNDLEGENDE Anforderungen für die Verbesserung Ihrer Java-Apps

BASIC ist ganz einfach eine Grundsprache. Es gibt zwei Denkschulen, wie man einen Dolmetscher dafür schreiben könnte. Ein Ansatz besteht darin, eine Programmierschleife zu schreiben, in der das Interpreterprogramm eine Textzeile aus dem interpretierten Programm liest, analysiert und dann eine Unterroutine aufruft, um sie auszuführen. Die Sequenz des Lesens, Parsens und Ausführens wird wiederholt, bis eine der Anweisungen des interpretierten Programms den Interpreter auffordert, anzuhalten.

Die zweite und viel interessantere Möglichkeit, das Projekt in Angriff zu nehmen, besteht darin, die Sprache in einen Analysebaum zu analysieren und den Analysebaum dann "an Ort und Stelle" auszuführen. So funktionieren Tokenizing-Dolmetscher und so habe ich mich entschieden, fortzufahren. Tokenizing-Interpreter sind auch schneller, da sie die Eingabe nicht jedes Mal neu scannen müssen, wenn sie eine Anweisung ausführen.

Wie oben erwähnt, sind die drei Komponenten, die zum Erreichen einer dynamischen Ausführung erforderlich sind, ein Mittel zum Laden, ein Modulformat und die Ausführungsumgebung.

Die erste Komponente, ein Mittel zum Laden, wird von einem Java behandelt InputStream. Da Eingabestreams in der E / A-Architektur von Java von grundlegender Bedeutung sind, kann das System ein Programm von einem einlesen und in eine InputStreamausführbare Form konvertieren. Dies stellt eine sehr flexible Möglichkeit dar, Code in das System einzugeben. Natürlich ist das Protokoll für die Daten, die über den Eingabestream gehen, BASIC-Quellcode. Es ist wichtig zu beachten, dass jede Sprache verwendet werden kann. Machen Sie nicht den Fehler zu glauben, dass diese Technik nicht auf Ihre Anwendung angewendet werden kann.

Nachdem der Quellcode des interpretierten Programms in das System eingegeben wurde, konvertiert das System den Quellcode in eine interne Darstellung. Ich habe mich für den Analysebaum als internes Darstellungsformat für dieses Projekt entschieden. Sobald der Analysebaum erstellt wurde, kann er bearbeitet oder ausgeführt werden.

Die dritte Komponente ist die Ausführungsumgebung. Wie wir sehen werden, sind die Anforderungen für diese Komponente recht einfach, aber die Implementierung weist einige interessante Wendungen auf.

Eine sehr schnelle BASIC Tour

Für diejenigen unter Ihnen, die vielleicht noch nie von BASIC gehört haben, gebe ich einen kurzen Einblick in die Sprache, damit Sie die bevorstehenden Herausforderungen beim Parsen und Ausführen verstehen können. Für weitere Informationen zu BASIC empfehle ich die Ressourcen am Ende dieser Spalte.

BASIC steht für Beginners All-Purpose Symbolic Instructional Code und wurde an der Dartmouth University entwickelt, um Studenten im Grundstudium Rechenkonzepte beizubringen. Seit seiner Entwicklung hat sich BASIC zu einer Vielzahl von Dialekten entwickelt. Der einfachste dieser Dialekte wird als Steuerungssprache für industrielle Prozesssteuerungen verwendet. Die komplexesten Dialekte sind strukturierte Sprachen, die einige Aspekte der objektorientierten Programmierung beinhalten. Für mein Projekt habe ich einen Dialekt namens BASIC-80 gewählt, der Ende der siebziger Jahre auf dem CP / M-Betriebssystem beliebt war. Dieser Dialekt ist nur mäßig komplexer als die einfachsten Dialekte.

Anweisungssyntax

Alle Anweisungszeilen haben die Form

[: [: ...]]

where "Line" is a statement line number, "Keyword" is a BASIC statement keyword, and "Parameters" are a set of parameters associated with that keyword.

The line number has two purposes: It serves as a label for statements that control execution flow, such as a goto statement, and it serves as a sorting tag for statements inserted into the program. As a sorting tag, the line number facilitates a line editing environment in which editing and command processing are mixed in a single interactive session. By the way, this was required when all you had was a teletype. :-)

While not very elegant, line numbers do give the interpreter environment the ability to update the program one statement at a time. This ability stems from the fact that a statement is a single parsed entity and can be linked in a data structure with line numbers. Without line numbers, often it is necessary to re-parse the entire program when a line changes.

The keyword identifies the BASIC statement. In the example, our interpreter will support a slightly extended set of BASIC keywords, including goto, gosub, return, print, if, end, data, restore, read, on, rem, for, next, let, input, stop, dim, randomize, tron, and troff. Obviously, we won't go over all of these in this article, but there will be some documentation online in my next month's "Java In Depth" for you to explore.

Each keyword has a set of legal keyword parameters that can follow it. For example, the goto keyword must be followed by a line number, the if statement must be followed by a conditional expression as well as the keyword then -- and so on. The parameters are specific to each keyword. I'll cover a couple of these parameter lists in detail a bit later.

Expressions and operators

Often, a parameter specified in a statement is an expression. The version of BASIC I'm using here supports all of the standard mathematical operations, logical operations, exponentiation, and a simple function library. The most important component of the expression grammar is the ability to call functions. The expressions themselves are fairly standard and similar to the ones parsed by the example in my previous StreamTokenizer column.

Variables and data types

Part of the reason BASIC is such a simple language is because it has only two data types: numbers and strings. Some scripting languages, such as REXX and PERL, don't even make this distinction between data types until they are used. But with BASIC, a simple syntax is used to identify data types.

Variable names in this version of BASIC are strings of letters and numbers that always start with a letter. Variables are not case-sensitive. Thus A, B, FOO, and FOO2 are all valid variable names. Furthermore, in BASIC, the variable FOOBAR is equivalent to FooBar. To identify strings, a dollar sign ($) is appended to the variable name; thus, the variable FOO$ is a variable containing a string.

Finally, this version of the language supports arrays using the dim keyword and a variable syntax of the form NAME(index1, index2, ...) for up to four indices.

Program structure

Programs in BASIC start by default at the lowest numbered line and continue until there are either no more lines to process or the stop or end keywords are executed. A very simple BASIC program is shown below:

100 REM This is probably the canonical BASIC example 110 REM Program. Note that REM statements are ignored. 120 PRINT "This is a test program." 130 PRINT "Summing the values between 1 and 100" 140 LET total = 0 150 FOR I = 1 TO 100 160 LET total = total + i 170 NEXT I 180 PRINT "The total of all digits between 1 and 100 is " total 190 END 

The line numbers above indicate the lexical order of the statements. When they are run, lines 120 and 130 print messages to the output, line 140 initializes a variable, and the loop in lines 150 through 170 update the value of that variable. Finally, the results are printed out. As you can see, BASIC is a very simple programming language and therefore an ideal candidate for teaching computation concepts.

Organizing the approach

Typical of scripting languages, BASIC involves a program composed of many statements that run in a particular environment. The design challenge, then, is to construct the objects to implement such a system in a useful way.

When I looked at the problem, a straightforward data structure fairly leaped out at me. That structure is as follows:

The public interface to the scripting language shall consist of

  • A factory method that takes source code as input and returns an object representing the program.
  • An environment that provides the framework in which the program executes, including "I/O" devices for text input and text output.
  • A standard way of modifying that object, perhaps in the form of an interface, that allows the program and the environment to be combined to achieve useful results.

Internally, the structure of the interpreter was a bit more complicated. The question was how to go about factoring the two facets of the scripting language, parsing and execution? Three groups of classes resulted -- one for parsing, one for the structural framework of representing parsed and executable programs, and one that formed the base environment class for execution.

In the parsing group, the following objects are required:

  • Lexikalische Analyse zur Verarbeitung des Codes als Text
  • Ausdrucksanalyse, um Analysebäume der Ausdrücke zu erstellen
  • Anweisungsanalyse, um Analysebäume der Anweisungen selbst zu erstellen
  • Fehlerklassen zum Melden von Fehlern beim Parsen

Die Framework-Gruppe besteht aus Objekten, die die Analysebäume und die Variablen enthalten. Diese schließen ein:

  • Ein Anweisungsobjekt mit vielen speziellen Unterklassen zur Darstellung geparster Anweisungen
  • Ein Ausdrucksobjekt zur Darstellung von Ausdrücken zur Auswertung
  • Ein variables Objekt mit vielen speziellen Unterklassen zur Darstellung atomarer Dateninstanzen