Der BASIC - Interpreter der KC-Reihe


Der Interpreter besteht aus einer Vielzahl von Routinen und Tabellen. Hinter den Eintrittspunkten für Kalt- und Warmstart existierert eine Liste aller reservierten Wörter und nachfolgend die dazugehörigen Sprungtabellen. Der Kernteil der Tabellen ist für alle KC-Typen einheitlich gestaltet, der hintere Teil ist speziell den Rechnertypen KC 85/2 bis KC 85/4 bzw. KC 85/1, KC 87 angepaßt.
  1. Unterschiede der Rechnertypen
  2. Speicheraufteilung
  3. REM
  4. LET
  5. Variablen und ihre Besonderheiten
  6. Sprunganweisungen GOTO und GOSUB
  7. Die FOR - Schleife
  8. Logische Verknüpfungen
  9. DATA - Zeilen

1. Unterschiede der Rechnertypen

Die Rechner aus der Produktion von Robotron (Z 9001, KC 85/1 und KC 87) sind den Modellen aus Mühlhausen (KC 85/2 bis KC 85/4) in den Grundzügen sehr ähnlich. Unterschiede treten nur im erwähnten hinteren Teil des Interpreters auf. Bei den Befehlen INKEY$, JOYST, STRING$, INSTR$, RENUMBER, DELETE, PAUSE, BEEP, WINDOW, BORDER, INK, PAPER und AT treten aber schon Unterschiede auf. Bei PRINT AT ist es den Mühlhausenern Rechner möglich, mittels TAB und SPC, Komma und Semikolon Formatierungen in der Ausgabe durchzuführen. Die Robotron-Rechner quittieren diese Anweisungen mit Fehlermeldungen.

Befehle wie beispielsweise COLOR, LINE, PSET, PRESET oder CIRCLE aber  auch Befehle zur Peripherie-Steuerung, z.B. OPEN, CLOSE, SWITCH, sind, den Konzepten der zugrunde liegenden Hardware bedingt, nur auf den Computern aus Mühlhausen verfügbar.

Die sichtbaren Unterschiede betreffen die Grafikfähigkeiten:
 
Mühlhausen Robotron
40 Zeichen 40 Zeichen
32 Zeilen 22 Zeilen
320 * 256 Pixel 128 Sonderzeichen (Blockgrafik)
Farbbyte für 4 * 8 Pixel Farbbyte für jedes Zeichen


2. Speicheraufteilung

Hier sei kurz die Speicherbelegung bzgl. Basic der Modelle aus Mühlhausen erläutert (im Standardzustand ohne Speichermodule). 

Von Adresse 300H bis 3FFH liegen die Arbeitszellen des Basic-Interpreters. Sie enthalten Sprungbefehle für den Warmstart, E/A-Flags, Zufallszahlen, Prüfsummen für Lade und Speicherbefehle, die aktuelle Zeilenummer, die Programmstart-Adresse, die Cursorposition, die nächste freie String-Adresse, die String-Adreßtabelle usw.

Das Basicprogramm wird im Speicher ab 401H abgelegt. Unmittelbar hinter dem BASIC-Programm schließt sich  der Raum für Variablen, Arrays an. Strings und der Stack werden am Ende des freien Speicherraums abgelegt. Ihr Speicherbereich dehnt sich je nach Bedarf in die absteigenden Adreßbereiche aus.
 
Adresse
3FFFH String belegt
String frei
Stack
dynamisch freier Speicher
Array
Varibalen
401H - ... BASIC-Programm
300H - 3FFH BASIC-Arbeitszellen

Startarten des Interpreters

An Startarten des Interpreters existieren zwei Varianten.
  1. Kaltstart
  2. Warmstart
Beim Kaltstart werden die Arbeitszellen des Interpreters mit Default-Werten belegt. Diese umfassen z.B. die Arbeitspeichergröße, den Zufallsgenerator etc.

Der Warmstart beläßt den Arbeitsspeicher in seinem aktuellen Zustand, so daß ein vorhandenes Programm im Arbeitsspeicher nicht verlorengeht und die Arbeit wie vor Verlassen des Interpreters fortgesetzt werden kann.

Eingabe und Start eines Programms

Bei der Eingabe einer BASIC-Zeile wird sie zunächst im Eingabepuffer als ASCII-Text gehalten. Mit Betätigung der Enter-Taste wird eine Routine zur Umwandlung der Zeile in die interne Darstellung aufgerufen. Dabei wird im Text nach reservierten Wörtern durchsucht und diese werden in Token umgewandelt. Jedem reservietem Wort ist ein spezielles Byte größer 7FH als Token zugewiesen. Token bieten den Vorteil, daß durch sie der Programmtext verkürzt wird und die Abarbeitung des Programms beschleunigt wird. Die Zeilennummer wird als einzige Zahl in ihre äquivalente binäre Darstellung gewandelt, alle anderen auftretenden Zahlen behalten ihre ASCII-Darstellung.

Als Besonderheit kann bei der Programmeingabe das "?" als Ersatz für den PRINT-Befehl genutzt werden. Dieses Fragezeichen wird durch besagte Routine in ein richtiges PRINT umgesetzt und als PRINT-Token gespeichert.

Eine weitere Routine ordnet den so entstandenen Programmkode in den Programmspeicher ein, wobei auch das Einsortieren bzgl. Zeilennummer erfolgt. Zum Schluß wird die Zeile noch um zwei Byte ergänzt, die als Zeiger auf den Beginn der nächsten Zeile dienen.

Der Aufbau einer Zeile sieht nun wie folgt aus
 
Adresse 
Pointer
Zeilenummer
Programtext
Endekennung
401
0F 04
0A 00
10
9E 20 22  68  61 6C 6C 6F 22 
PRINT "HALLO"
00
40F
00 00

Folgt keine weitere Programmzeile, so wird der Pointer mit 00 00 gefüllt.

Start eines Programms

Der Start eines Programms umfaßt zwei Teile, zum einen den 
  1. Initialisierungslauf und 
  2. den für den Nutzer sichtbaren Teil. 
Der Initialiseirungsteil testet das Programm auf Eigenschaften, die für den Ablauf des Programms von Bedeutung sind. Dieser Test umfaßt dioe Ermittlung des notwendigen Speicherplatzes für Variablen, Arrays, Strings, die Lage des Stacks und ermittelt die erste DATA-Zeile. Alle diese ermittelten Werte werden in den Arbeitszellen des Interpretres vermerkt.

Bei der Initialisierung wird nur der erforderliche Speicher zur späteren Ablage der Variablen ermittelt und mit Nullen belegt. Eine Festlegung, wie die Variablen, Felder oder Strings in dem reservierten Bereich abgelegt werden sollen wird hier nicht getroffen.


3. REM

REM kennzeichnet Kommentare im Programm. Alles nach REM folgende wird überlesen und nicht interpretiert. Allerdings hat jedes REM zeitliche Auswirkungen, so verursacht ein REM eine Verzögerung im Programmablauf von ca. 1ms. Alternativ sollten daher alle REM am Ende eines Programms untergebracht werden, um so den Programmfluß nicht zu verlangsamen.

4. LET

Zuweisungen in BASIC können optional mit LET erfolgen, z.B. LET X=1. Äquivalent dazu ist die Anweisung X=1. Der ursprüngliche Gedanke war, Zuweisungen und Vergleiche zu unterscheiden.

5. Variablen und ihre Besonderheiten

Numerische Variablen

Numerische Variablen werden in sechs Bytes kodiert. 
 
Name Zahlenwert
Byte 1 2 3 4 5 6

In den ersten zwei Bytes wird der Name der Variablen nach folgender Regel kodiert: Das erste Byte ist 0, falls nur ein Buchstabe verwendet wird.
 
nur ein Buchstabe verwendet
  • erste Byte ist 0, zweite Byte erhält die Kodierung des einen Zeichens
  • Bsp.: A = 00 41
sonst
  • erste Byte gleich dem zweiten
  • Bsp.: AB = 42 41

In den verbleibenden vier Bytes wird der Wert der Variablen in gleitkommadarstellung mit Mantisse und Exponent verschlüsselt. Absolute Zahlenwerte liegen zwischen 9,4*10-39 und 1,7*1038, der größte Integerwert beläuft sich auf 3,35544*107.

Zu Beginn des RUN ist der Variablenraum mit Nullen belegt. Stößt der Interpreter bei der Abarbeitung des Programms auf eine Variable, so legt er diese im reservierten Bereich ab. Dies geschieht immer nach dem selben Schema:

  1. Suche ob die Variable schon angelegt wurde; Suche erfolgt vom Beginn des Variablenraumes
  2. ist sie vorhanden, kann dort gelesen und geschrieben werden
  3. ist sie noch nicht vorhanden, wird sie hinter die bereits vorhandene Variablen geschrieben
Bei jeder Suche wird der Varibalenraum immer von Anfang an durchsucht. Daher sollten häufig benutzte Variablen als erstes initialisiert werden. 

Stringvariablen

String-Variablen werden durch ein abschließendes "$" identifiziert und ähnlich wie numerische Variabaen gespeichert. Der Name wird ebenfalls in den ersten zwei Bytes abgelegt. Allerdings wird hier das erste Byte um 80H erhöht.
AB$ = C2 41 anstelle von 42 41
Erfaßt wird das Dollar-Zeichen als Kennzeichung einer String-Variablen also indirekt durch das Aufaddieren von 80H auf die HEX-Kodierung des zweiten Zeichens.

Das dritte Byte beinhaltet die Zeichenanzhal des Strings. Im fünften und sechsten Byte wird der Beginn des Strings im RAM als Pointer gespeichert.
 
Name Zeichenzahl des Strings 00 Adresse, ab der der String abgelegt ist
Byte 1 2 3 4 5 6

Strings werden also indirekt abgelegt. Dies hat einen entscheidenen Vorteil zur Folge: Bei der Initialisirung muß nur die Anzahl der numerischen und String-Variablen gezählt und mit 6 multipliziert werden, um die notwendige Speichergröße für den Variablenraum zu erhalten.

Die Ablage des Stringtextes erfolgt in einem separaten Adressraum. Strings können auf unterschiedliche Weise erzeugt werden:

  1. im Programm selbst, z.B. A$="Hallo"
  2. durch Zuweisung zu Stringvariablen, z.B. READ A$
  3. durch Verknüpfung, z.B. A$=B$+C$
Im ersten Fall hat der String eine konstante Länge. Er wird dann nicht extra gespeichert, sondern es wird nur der Pointer (Byte 5 und 6) auf seine Position im Quelltext gesetzt und seine Länge im Byte 3 eingetragen.

Im zweiten und dritten Fall kann die Länge des Strings variieren. Deshalb wird der Inhalt des Strings nicht am alten Speicherplatz abgelegt werden, da sonst die Gefahr des Überschreibens anderer Strings besteht. Statt dessen wird jeder neu belegte String auf einen noch freien Speicherplatz abgelegt. Seine Adresse und seine Länge in den letzten vier Byte abgelegt. Der alte Text bleibt dabei als "Leiche" im Speicher erhalten. 

Abhilfe schafft hier die garbage collection. Sie wird vom Interpreter aufgerufen, wenn nicht mehr genügend freier Stringraum zur Verfügung steht. Bentuzte Strings werden an den Anfang des Speicherbereiches verschoben und ihre Pointer aktualisiert, die alten Belegungen werden dabei gelöscht. Die garbage collection kann sich als sehr zeitaufwendige Angelegenheit entpuppen. Der zeitliche Aufwand umfaßt zwei Zeiten

  1. die Dauer einer garbage collection
  2. Abstand zwischen zwei garbage collection
Die Dauer ist hauptsächlich von der Anzahl der Strings abhängig, denn das Verändern der Pointer nimmt die größte Zeitspanne in Anspruch.
 
Anzahl Strings 50 100 250 500 1000
Dauer in Sekunden 0,4 1,7 11 41 165

Der Abstand wächst mit der Differenz zwischen dem vorhanden Stringraum und dem Speicherbedarf für die Strings - er ist also von der Hardware abhängig. Andererseits hat auch die Häufigkeit der Neubelegung von Strings einen entscheidenen Einfluß.

Arrays

Arrays werden in einem separatem Speicherbereich abgelegt. Zusammengehörige Werte verwenden denselben Namen. Im Anschluß der Namenskodierung  und den Informationen über die Dimensionen folgen jeweils 4 Byte je Feldelement.

Beispiel: DIM A(1,2,3)
 
Name Länge Dimension Anzahl Variablen je Dimension Anzhal Bytes
4 Byte je Zahl
00 41 67 00 03 04 00 03 00 02 00 (2*3*4)*4 = 60H

Für jedes Feld existieren:

  • der Name (gebildet wie bei Variablen)
  • die Länge des Feldes
  • Dimensionenanzahl
  • notwendige Byteanzahl für die Werte

Bestimmungen für Variablennamen

Variablennamen beliebiger Länge werden mit derf Einschränkung unterstützt, daß für die Speicherung und Unterscheidung der Varinbalen nur die ersten zwei Zeichen genutzt werden. So sind die Variablen "HALLO" bzw "HALLO2" in der Verarbeitung gleich und identifizieren somit dieselbe Variable.

Die Regel zur Erzeugung eines Varibalennamens ist daher recht einfach

  • Beginn mit einem Großbuchstaben
  • Fortsetzung mit einem Buchstaben oder einer Ziffer
Reservierte Wörter wie AT, FN, IF, LN, ON, OR, PI , TO dürfen nicht im Namen vorkommen.

4. Sprunganweisungen GOTO und GOSUB

Bei diesen Befehlen besteht das Problem hauptsächlich in der Zeilensuche der anzuspringenden Zahl. 

Was geschieht nun bei GOTO n ?

  1. Als erstes wird die als ASCII-Werte vorliegende Zeilenzahl eingelsen, in ihr binäres Äquivalent gewandelt und im Arbeitsspeicher abgelegt.
  2. Der Pointer der ersten Zeile wird zwischengespeichert.
  3. m sei die Zeilenzahl hinter dem Pointer. Die gesuchte Zeilenzahl wird mit m verglichen. Dabei können drei Fälle auftreten:
    1. n = m  - Sprungziel, Abarbeitung ab hier
    2. n > m  - gesuchte Zeile nicht existent, Fehler
    3. n < m  - gehe zu 4.
  4. Der Pointer wird wieder zurückgeholt und mit der Zeile auf die er zeigt wird fortgefahren. Dieser Pointer wird wieder zwischengespeichert und die Auswertung mit Punkt 3 fortgesetzt.
Hier ist deutlich zu sehen, daß bei der Suche nach einer Zeilenummer immer am Angang des Programms begonnen wird. Je Zeilensuche werden ca. 0,1 ms benötigt, was für kurze Programme natürlich nicht von Bedeutung ist, aber bei größeren Projekten Wirkung zeigen kann und auch wird.

Es ist daher anzuraten, Sprünge, ob nun mit GOTO oder Unterprogrammaufrufe mit GOSUB, immer soweit wie möglich nach vorne zu legen, damit somit die Suchzeit minimal gehalten werden kann. Das gleiche Problem war schon bei den Variablen zu sehen.

Beim Aufruf von GOSUB wird die hinter GOSUB Zeilenzahl stehende Adresse auf den Stack gelegt und die Zeilensuche wie bei GOTO erläutert durchgeführt.

Bei Erreichen von RETURN wird die Adresse vom Stack geholt und dann an dieser Stelle weitergearbeitet. Das RETURN ist somit sehr schnell und fällt daher kaum ins Gewicht.


5. Die FOR - Schleife

Die FOR-Schleife hat folgenden Aufbau:
 
FOR X=Anfangswert TO Endwert STEP Schrittweite
Schleifenrumpf
NEXT
Vor dem Schleifenrumpf wird X mit dem Startwert initialisiert, es läuft der Schleifenrumpf ab und bei Erreichen von NEXT X laufen nachflgende Punkte ab:
  1. X wird um die Schrittweite erhöht. Wurde beim Schleifenkopf die Anweisung STEP weggelassen,so wird die Schrittweise auf den Defaultwert 1 gesetzt.
  2. Test ob X den Endwert erreicht hat
  3. Abhängig vom Ergebnis wird die Schleife nochmals durchlaufen oder hinter NEXT X weitergearbeitet.
Endwert und Schrittweite werden zu Beginn übernommen und auf den Stack gelegt, X wird der Startwert zugewiesen. Anfangswert, Endwert und Schrittweite haben nun keinen Einfluß mehr auf die Abfolge der Schleife und können innerhlab dieser beliebig geändert werden. Die Laufvariable ist nach dem NEXT um enie Schrittweite größer als zuletzt in der Schleife.

Einzig ausschlaggebende Veränderung ist die der Laufvariablen X. Soll beispielsweise die Schleife vorzeitig beendet werden, so ist es ratsam, nicht einfach aus der Schleife mittels GOTO herauszuspringen. Diese Vorgehensweise hinterläßt einen noch intakten Stack mit den Werten von Endwert und Schrittweite. Sicherer und damit korrekt ist es, die Laufvariable auf ihren Endwert zu setzen, um damit einen Abbruch zu erzeugen.

Die Schleif wird in jedem Fall einmal durchlaufen. Um ihre Abarbeitung dennoch zu unterbinden, kann die IF - THEN - Anweisung eingesetzt werden.

Im Zusammenspiel von STEP und einer IF - Anweisung am Ende des Schleifenrumpfes kann eine While - Schleife nachgebildet werden. Die Schrittweite wird mittels STEP 0 auf Null gestzt. Damit ist gewährleistet, daß die Laufvariable immer ihren Startwert behält. Am Ende des Schleifenrumpfes wird nun auf die Abbruchbedingung getetstet, tritt sie ein, wird die Laufvaribale auf ihren Endwert gesetzt und die Schleife wird abgebrochen.

NEXT kann auch ohne Laufvariable genutzt werden kann. In diesem Fall wird bei Erreichen von NEXT die letzte Laufvariable im Stack benutzt und erhöht. Bei NEXT X hingegen wird der Stack nach der Varibalen X durchsucht und bis zu ihrer Position abggebaut. X wird nun erhöht. Ist keine Schleifenvariable mit dem Namen X vorhanden, erfolgt eine Felhlermeldung "?NF ERROR IN xxx" (next without for). 

Bei geschachtelten Schleifen ist daher immer die Reihenfolge des NEXT zu beachten:

FOR X=XA TO XE
...
FOR Y=AY TO EY
...
NEXT Y
NEXT X
Wären NEXT X und NEXT Y vertauscht, also 
FOR X=XA TO XE
...
FOR Y=AY TO EY
...
NEXT X
NEXT Y
so könnte NEXT Y nicht mehr ausgeführt werden, da Y nicht mehr auf dem Stack vorhanden ist, und es würde zur Fehlermeldung "?NF ERROR IN xxx" kommen.
 

6. Logische Verknüpfungen

Das KC-BASIC besitzt keine besonderen logischen Varibalen. Logische Varinblen werden hier indirekt erzeugt. 

BASIC stellt als logische Verknüpfen den üblichen Standard zur Verfügung, als da wären

  • <
  • >
  • <=
  • >=
  • <>
Bei logischen Verknüpfungen dürfen an allen Stellen numerische Variablen auftreten, wo eigentlich nur die Verwendung von logischen Größen gestattet ist.

Logische Werte werden nicht durch die Boolean-Werte true oder false dargestellt, sonder durch -1 und 0, wobei 

  • wahr   = -1
  • falsch =   0
Diese Werte kann man auch numerischen Varibalen zuordnen, z.B. Wahrheitswert = X < Y. Eine besondere Form ist die Form mit zwei Gleichheitszeichen X = Y = Z. Hierbei wird Y = Z auf Gleichheit getestet und die daraus resultierende logische Größe X zugewiesen.

Logische Operationen verwenden also nur ganze Zahlen und nur im Bereich von -1 bis 0 liefern "IF X" und "IF NOT X" eindeutige Aussagen. Bei anderen Zahlenbereichen sind beide Aussagen wahr.


9. DATA - Zeilen

Zur Ablage von konstanten Daten, die oft genutzt werden, können DATA - Zeilen benutzt werden, z.B. DATA 12, Hallo. Die Besonderheit besteht darin, das Strings ohne Anführungszeichen abgelegt werden. Somit ist es möglich, Strings auch Zahlen zuzuweisen. Eine gemischte Ablage von Strings und Zahlen in einer DATA - Zeile ist zulässig.

Ein RUN setzt den Zeiger der DATA - Zeilen immer auf die erste dieser Zeilen. Sollte im Programmlauf eine spezielle DATA - Zeile benötigt werden, so kann diese mittels RESTORE n angesprochen werden.

10 DATA ...
20 DATA 12, HALLO
...
100 RESTORE 20
110 READ A
In obigem Beispiel wird der DATA - Zeiger auf die Zeile 20 gesetzt und A erhält den Wert 12. Dies geschieht durch die Anweisung READ, die den aktuellen DATA - Wert liefert, der Zeiger wird nach dem Lesen auf den nächsten Wert gesetzt. RESTORE ohne Parameter setzt den DATA - Zeiger auf die erste DATA - Zeile.