(1 parte)
from: http://ch.twi.tudelft.nl/~sidney/atari/turbo-basic/compiler-internals.html

So funktioniert der Turbo-Basic-Compiler

Soll ein Basic-Programm schneller werden, dann greift man einfach zum Basic-Compiler. Aber wie funktioniert ein solches Beschleunigungswunder?

Manche Anwendungen erfordern mehr Tempo, als reine Basic-Programme zu bieten haben. Dann schreibt man zeitkritische Routinen entweder in Maschinensprache oder setzt einen Compiler ein, der ein Basic-Programm in Maschinensprache umwandelt. Letzteres ist natürlich viel einfacher und bequemer, da es lediglich den Umgang mit dem Compiler vorraussetzt. Außerdem benötigt die Compilierung nicht viel Zeit. Das Compilat kann übrigens nicht mehr von Basic aus bearbeitet werden. Wenden wir uns nun der Funktionsweise des Turbo-Basic-Compilers zu.

Wenn Sie ein Basic-Programm eintippen, zum Beispiel die Programmzeile »100 ?3*4«, faßt der Computer diese Zeile zunächst nur als eine Folge von Zeichen im ATASCII-Format auf. Unter dem Begriff ATASCII versteht man die spezielle Atari-Version des ASCII-Zeichensatzes (ASCII ist die Abkürzung für: American Standard Code for Information Interchange). Der Interpreter wandelt den eingegebenen Text in Token um. Gleichzeitig überprüft er noch die Syntax, also ob die Befehlsfolge auch zulässig ist. Diese Aufgabe muß meistens auch ein Compiler übernehmen. Bild 1 zeigt, wie die Programmzeile »100 ?3*4« in Token übersetzt aussieht.

Die Programmzeile wird dann anhand ihrer Nummer einsortiert. Durch diese Umwandlung in Token und gleichzeitiger Syntaxprüfung wird die Aufgabe des Compilers etwas einfacher. Speichert man ein Basic-Programm mit dem SAVE-Befehl, werden auch die Token gespeichert. Mit LIST hingegen liegt es in der Textform vor. Der Turbo-Basic-Compiler kann nur die »SAVE-Ausführung« übersetzen. Ein Transfer der TextForm wäre prinzipiell auch möglich, allerdings würde der Compiler hierfür mehr Zeit benötigen. Schließlich muß er die Befehlsnamen kennen und die Variablennamen speichern. Dies würde weiterhin die maximale Länge der zu compilierenden Programme vermindern. Oder es müßten Zwischenfiles auf Diskette angelegt werden, was bei den relativ langsamen Atari-Diskettenlaufwerken sehr zeitaufwendig ist.

Keine Variable ohne Nummer

Bevor Sie ein Basic- oder Turbo-Basic-XL-Programm compilieren, sollten Sie sicherstellen, daß es fehlerfrei läuft. Zwar ist dann immer noch nicht gewährleistet, daß das Programm anschließend auf Anhieb ordnungsgemäß compiliert und ausgeführt werden kann. Zumindest entfallen aber Syntax-Fehler, die zur Folge hätten, daß Sie Ihr Basic-Programm korrigieren müßten, um es anschließend nochmals zu compilieren. Auch muß die Struktur Ihrer Basic-Programme stimmen. Dazu bietet sich der LIST-Befehl unter Turbo-Basic XL an, der bekanntlich FOR-NEXT-Schleifen und IF-ELSE-ENDIF Abfragen einrückt.

Atari-Basic und Turbo-Basic XL-SAVE-Files bestehen aus einem Vorspann mit den Variablennamen und einigen anderen Informationen für den Interpreter. Diese werden deshalb vor dem Einlesen entweder überlesen (wie die Variablennamen) oder, in einer angepaßten Form, gespeichert. Dies betrifft in erster Linie die Variablentypen. Der Variablentyp kann aber nicht nur aus der Variablennummer, die im Programm gespeichert ist, definiert werden. Das Dollarzeichen ($) beispielsweise, welches die Stringvariablen kennzeichnet, wird vom Interpreter nicht im eigentlichen Programm gespeichert.

Bei langen Programmen mit vielen Variablen kann man diese Phase der Compilation deutlich erkennen. In der Info-Zeile (der dritten Bildzeile des Turbo-Basic-Compilers) steht dann nur das Wort Zeile ohne Zeilennummer. Sobald die erste Zeile übersetzt wird, erscheint auch die Anzeige der entsprechenden Zeilennummer.

Jetzt beginnt die eigentliche Compilierung. Sie setzt sich aus zwei Durchgängen zusammen. Im ersten Durchgang (Paß 1) wird das Programm Zeile für Zeile und Befehl für Befehl eingelesen und in Maschinencode übersetzt. Im zweiten Durchgang (Paß 2) werden dann noch die offenen Sprungadressen eingesetzt. Dazu werden im Paß 1 GOTO-Sprünge im Code durch einen ungültigen Maschinenbefehl mit angehängter Zeilennummer gekennzeichnet. Im zweiten Durchgang schließlich ersetzen dann 6502-JMP-Befehle (Code $4C) die ungültigen Maschinenbefehle.

Ähnliches gilt für Zahlen und Textkonstanten. Sie werden in einem Block hinter dem eigentlichen Basic-Programm gespeichert. Die endgültigen Adressen stehen dann erst nach der Übersetzung fest. Im Anschluß an den zweiten Compilier-Durchgang folgt dann noch eine Kontrolle, ob offene Schleifen vorliegen. Also ob ein NEXT zu einem FOR fehlt oder ein WEND zu einem WHILE etc. Trifft dies zu, erscheint eine Meldung auf dem Bildschirm. Dann folgt noch die Überprüfung nach fehlenden ENDIFs. Durch diese Aufteilung der Fehlerprüfung (die meisten Fehler werden bereits im ersten Durchgang festgestellt) werden die fehlerhaften Zeilennummern nicht komplett sortiert ausgegeben, sondern gegebenenfalls in vier einzelnen Gruppen.

Die meiste Arbeit wird im ersten Durchgang geleistet, der Rest dauert nur wenige Sekunden. Das EinLesen des Programms erfolgt Zeile für Zeile. Zuerst zur Zeilennummer: Wenn die Zeilennummer größer als 32767 ist, dann ist Paß 1 beendet (der Interpreter verwendet die Zeilennummer 32768 übrigens als Endekennzeichen). Dann schließt der zweite Durchgang an. Ansonsten wird die Zeilennummer angezeigt und die Zeile Befehl für Befehl eingelesen und übersetzt.

Sollte der Compiler auf einen LIST- oder ENTER-Befehl stoßen, wird einfach eine Fehlermeldung mit Zeilennummer ausgegeben, und der nächste Befehl kommt an die Reihe.

Keinerlei Probleme bereiten REM- oder »--«-Zeilen. Diese Befehle werden einfach überlesen und ignoriert, da sie den Programmablauf nicht beeinflussen.

Befehle ohne Parameter (wie END, POP oder CLR) werden einfach zu einem entsprechenden Unterprogrammaufruf compiliert.

Bei einem DATA-Befehl wird der Text mit Zeilennummer und Länge in einer Tabelle im Anschluß an das Programm eingetragen. Sie nimmt noch folgende Informationen auf:

Diese Informationen werden gegebenenfalls auch im Speicher verschoben, wenn das zu compilierende Programm oder eine der Tabellen anwächst. Deshalb nimmt auch die Compilierungsgeschwindigkeit bei langen Programmen gegen Ende ab; vor allem wenn viele DATAs am Anfang des Programms stehen. Diese Verlangsamung betrifft lediglich die eigentliche Compilierung und nicht den Programmablauf.

Alle Befehle, die Parameter erfordern, sind wesentlich schwieriger zu übersetzen. Bild 2 zeigt nur einen Befehl mit einem einzigen Parameter.

Die Umwandlung des Befehls von Schritt 1 nach Schritt 2 hat der Interpreter schon erledigt. Der Compiler liest das Befehlstoken $03 für COLOR und verzweigt in die entsprechende Compilationsroutine. Diese ruft wiederum die Routine für die Übersetzung eines Ausdrucks auf, die eine Integerzahl zurückliefern soll. Dann wird nur noch »STA $C8« angehängt, und damit ist die Compilierung des Befehls schon erledigt. Beim Befehl GRAPHICS würde »JSR @Graphics« angehängt werden. Der Klammeraffe (@) kennzeichnet übrigens eine Routine in der Runtime Bibliothek.

Ein großer Fortschritt wurde bis jetzt noch nicht verzeichnet. Zwar sind die Befehle jetzt prinzipiell compiliert, das ändert aber leider nichts daran, daß mit dem entsprechenden Parameter, wie in unserem Beispiel die 1, noch nichts passiert ist.

Compilieren Zeile für Zeile

Die dafür benötigte Routine gliedert sich in mehrere Abschnitte:

P-Code (Pseudo-Code) ähnelt der Maschinensprache eines hypothetischen (gedachten) Prozessors. Manche Compiler erzeugen nur einen P-Code, der dann von einem kleinen Interpreter ausgeführt wird. Die Abarbeitungsgeschwindigkeit eines solchen Codes ist zwar schneller als ein herkömmliches Basic-Programm, allerdings von der Geschwindigkeit einer »richtigen« Maschinensprache noch weit entfernt. Aber platzsparender ist P-Code im Vergleich zu voll compiliertem Basic. Echte Maschinensprache ist jedoch von keinem Compiler zu übertreffen.

Bild 3 zeigt ein komplizierteres Beispiel. »03« entspricht wieder dem Token für COLOR, »46« steht für PEEK, »3A« für die offene Klammer, »2C« für die schließende Klammer, »35« steht für die Addition (+), »16« bedeutet Zeilenende oder auch einen Doppelpunkt, der Basic-Befehle in einer Zeile trennt. Der Wert »OE« kennzeichnet schließlich noch eine Zahl in der internen Darstellung (entspricht sechs Byte).

Das Token »03« ist in unserem Beispiel bereits identifiziert. Es beginnt jetzt der Test, in welcher Reihenfolge die Operationen ablaufen müssen. Der Interpreter hat dabei fast die gleichen Tests durchzuführen, und zwar jedesmal, wenn er diese Zeile erreicht. Der Compiler übersetzt jede Zeile nur einmal bei der Compilierung. Dadurch laufen die Programme schneller ab.

Bei der Übersetzung in P-Code bedient sich der Compiler einer Tabelle, in der die Prioritäten aller Operatoren vermerkt sind. Übrigens handelt es sich hierbei fast um die gleiche Tabelle, die auch der Interpreter benutzt. Daraus ergibt sich dann folgender P-Code. In Bild 4 sind die einzelnen Werte lediglich in Kurzform dargestellt. Der Compiler betrachtet sie natürlich als Zahlen.

Jetzt könnte theoretisch schon die Routine aufgerufen werden, die den Maschinencode erzeugt. Allerdings würden dann im compilierten Programm die gleichen Berechnungen ablaufen, die der Interpreter normalerweise auch durchführt. Schließlich steht jetzt viel Zeit zur Optimierung des P-Codes zur Verfügung, da die Programmzeilen nur ein einziges Mal in Maschinencode umgesetzt werden.

2 parte